├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── gen-sine │ └── main.go ├── metadata │ └── main.go ├── wavtagger │ └── main.go └── wavtoaiff │ └── main.go ├── cue_chunk.go ├── decoder.go ├── decoder_test.go ├── encoder.go ├── encoder_test.go ├── examples_test.go ├── fixtures ├── 32bit.wav ├── 8bit.wav ├── bass.wav ├── bloop.aif ├── bwf.wav ├── dirty-kick-24b441k.wav ├── flloop.wav ├── kick-16b441k.wav ├── kick.wav ├── listChunkInHeader.wav ├── listinfo.wav ├── misaligned-chunk.wav ├── padded24b.wav └── sample.avi ├── go.mod ├── go.sum ├── list_chunk.go ├── metadata.go ├── metadata_test.go ├── smpl_chunk.go └── wav.go /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | vendor/* 3 | !vendor/modules.txt 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7.6 4 | - 1.8.x 5 | - 1.9.x 6 | - 1.10.x 7 | 8 | sudo: false 9 | 10 | before_install: 11 | - go get -t -v ./... 12 | 13 | script: 14 | - go test -race -coverprofile=coverage.txt -covermode=atomic 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Matt Aimonetti 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wav codec 2 | 3 | [![GoDoc](https://godoc.org/github.com/go-audio/wav?status.svg)](https://godoc.org/github.com/go-audio/wav) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-audio/wav)](https://goreportcard.com/report/github.com/go-audio/wav) 5 | [![Coverage Status](https://codecov.io/gh/go-audio/wav/graph/badge.svg)](https://codecov.io/gh/go-audio/wav) 6 | [![Build Status](https://travis-ci.org/go-audio/wav.svg)](https://travis-ci.org/go-audio/wav) 7 | 8 | Wav audio file encoder and decoder. See [GoDoc](https://godoc.org/github.com/go-audio/wav) for more details. 9 | -------------------------------------------------------------------------------- /cmd/gen-sine/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "math" 7 | "os" 8 | 9 | "github.com/go-audio/wav" 10 | ) 11 | 12 | func main() { 13 | output := flag.String("output", "output.wav", "filename to write to") 14 | frequency := flag.Float64("frequency", 440, "frequency in hertz to generate") 15 | length := flag.Float64("lenght", 5, "length in seconds of output file") 16 | flag.Parse() 17 | 18 | log.Printf("generating a %f sec sine wav at %f hz", *length, *frequency) 19 | f, err := os.Create(*output) 20 | if err != nil { 21 | log.Fatalf("error creating %s: %s", *output, err) 22 | } 23 | defer f.Close() 24 | 25 | const sampleRate = 48000 26 | wavOut := wav.NewEncoder(f, sampleRate, 16, 1, 1) 27 | numSamples := int(sampleRate * *length) 28 | defer wavOut.Close() 29 | 30 | for i := 0; i < numSamples; i++ { 31 | fv := math.Sin(float64(i) / sampleRate * *frequency * 2 * math.Pi) 32 | v := uint16(fv * 32767) 33 | wavOut.WriteFrame(v) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/metadata/main.go: -------------------------------------------------------------------------------- 1 | // This tool reads metadata from the passed wav file if available. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/go-audio/wav" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) < 2 { 14 | fmt.Println("You must pass the pass the path of the file to decode") 15 | os.Exit(1) 16 | } 17 | f, err := os.Open(os.Args[1]) 18 | if err != nil { 19 | panic(err) 20 | } 21 | defer f.Close() 22 | dec := wav.NewDecoder(f) 23 | dec.ReadMetadata() 24 | if err := dec.Err(); err != nil { 25 | log.Fatal(err) 26 | } 27 | if dec.Metadata == nil { 28 | fmt.Println("No metadata present") 29 | return 30 | } 31 | fmt.Printf("Artist: %s\n", dec.Metadata.Artist) 32 | fmt.Printf("Title: %s\n", dec.Metadata.Title) 33 | fmt.Printf("Comments: %s\n", dec.Metadata.Comments) 34 | fmt.Printf("Copyright: %s\n", dec.Metadata.Copyright) 35 | fmt.Printf("CreationDate: %s\n", dec.Metadata.CreationDate) 36 | fmt.Printf("Engineer: %s\n", dec.Metadata.Engineer) 37 | fmt.Printf("Technician: %s\n", dec.Metadata.Technician) 38 | fmt.Printf("Genre: %s\n", dec.Metadata.Genre) 39 | fmt.Printf("Keywords: %s\n", dec.Metadata.Keywords) 40 | fmt.Printf("Medium: %s\n", dec.Metadata.Medium) 41 | fmt.Printf("Product: %s\n", dec.Metadata.Product) 42 | fmt.Printf("Subject: %s\n", dec.Metadata.Subject) 43 | fmt.Printf("Software: %s\n", dec.Metadata.Software) 44 | fmt.Printf("Source: %s\n", dec.Metadata.Source) 45 | fmt.Printf("Location: %s\n", dec.Metadata.Location) 46 | fmt.Printf("TrackNbr: %s\n", dec.Metadata.TrackNbr) 47 | 48 | fmt.Println("Sample Info:") 49 | fmt.Printf("%+v\n", dec.Metadata.SamplerInfo) 50 | for i, l := range dec.Metadata.SamplerInfo.Loops { 51 | fmt.Printf("\tloop [%d]:\t%+v\n", i, l) 52 | } 53 | for i, c := range dec.Metadata.CuePoints { 54 | fmt.Printf("\tcue point [%d]:\t%+v\n", i, c) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/wavtagger/main.go: -------------------------------------------------------------------------------- 1 | // This command line tool helps the user tag wav files by injecting metadata in 2 | // the file in a safe way. 3 | // All files are copied and stored in the wavtagger folder by the original files. 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/go-audio/wav" 15 | ) 16 | 17 | var ( 18 | flagFileToTag = flag.String("file", "", "Path to the wave file to tag") 19 | flagDirToTag = flag.String("dir", "", "Directory containing all the wav files to tag") 20 | flagTitleRegexp = flag.String("regexp", "", `submatch regexp to use to set the title dynamically by extracting it from the filename (ignoring the extension), example: 'my_files_\d\d_(.*)'`) 21 | // 22 | flagTitle = flag.String("title", "", "File's title") 23 | flagArtist = flag.String("artist", "", "File's artist") 24 | flagComments = flag.String("comments", "", "File's comments") 25 | flagCopyright = flag.String("copyright", "", "File's copyright") 26 | flagGenre = flag.String("genre", "", "File's genre") 27 | // TODO: add other supported metadata types 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | if *flagFileToTag == "" && *flagDirToTag == "" { 33 | fmt.Println("You need to pass -file or -dir to indicate what file or folder content to tag.") 34 | os.Exit(1) 35 | } 36 | if *flagFileToTag != "" { 37 | if err := tagFile(*flagFileToTag); err != nil { 38 | fmt.Printf("Something went wrong when tagging %s - error: %v\n", *flagFileToTag, err) 39 | os.Exit(1) 40 | } 41 | } 42 | if *flagDirToTag != "" { 43 | var filePath string 44 | fileInfos, _ := os.ReadDir(*flagDirToTag) 45 | for _, fi := range fileInfos { 46 | if strings.HasPrefix( 47 | strings.ToLower(filepath.Ext(fi.Name())), 48 | ".wav") { 49 | filePath = filepath.Join(*flagDirToTag, fi.Name()) 50 | if err := tagFile(filePath); err != nil { 51 | fmt.Printf("Something went wrong tagging %s - %v\n", filePath, err) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | func tagFile(path string) error { 59 | in, err := os.Open(path) 60 | if err != nil { 61 | return fmt.Errorf("failed to open %s - %w", path, err) 62 | } 63 | d := wav.NewDecoder(in) 64 | buf, err := d.FullPCMBuffer() 65 | if err != nil { 66 | return fmt.Errorf("couldn't read buffer %s %w", path, err) 67 | } 68 | in.Close() 69 | 70 | outputDir := filepath.Join(filepath.Dir(path), "wavtagger") 71 | outPath := filepath.Join(outputDir, filepath.Base(path)) 72 | os.MkdirAll(outputDir, os.ModePerm) 73 | 74 | out, err := os.Create(outPath) 75 | if err != nil { 76 | return fmt.Errorf("couldn't create %s %w", outPath, err) 77 | } 78 | defer out.Close() 79 | 80 | e := wav.NewEncoder(out, 81 | buf.Format.SampleRate, 82 | int(d.BitDepth), 83 | buf.Format.NumChannels, 84 | int(d.WavAudioFormat)) 85 | if err := e.Write(buf); err != nil { 86 | return fmt.Errorf("failed to write audio buffer - %w", err) 87 | } 88 | e.Metadata = &wav.Metadata{} 89 | if *flagArtist != "" { 90 | e.Metadata.Artist = *flagArtist 91 | } 92 | if *flagTitleRegexp != "" { 93 | filename := filepath.Base(path) 94 | filename = filename[:len(filename)-len(filepath.Ext(path))] 95 | re := regexp.MustCompile(*flagTitleRegexp) 96 | matches := re.FindStringSubmatch(filename) 97 | if len(matches) > 0 { 98 | e.Metadata.Title = matches[1] 99 | } else { 100 | fmt.Printf("No matches for title regexp %s in %s\n", *flagTitleRegexp, filename) 101 | } 102 | } 103 | if *flagTitle != "" { 104 | e.Metadata.Title = *flagTitle 105 | } 106 | 107 | if *flagComments != "" { 108 | e.Metadata.Comments = *flagComments 109 | } 110 | if *flagCopyright != "" { 111 | e.Metadata.Copyright = *flagCopyright 112 | } 113 | if *flagGenre != "" { 114 | e.Metadata.Genre = *flagGenre 115 | } 116 | if err := e.Close(); err != nil { 117 | return fmt.Errorf("failed to close %s - %w", outPath, err) 118 | } 119 | fmt.Println("Tagged file available at", outPath) 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /cmd/wavtoaiff/main.go: -------------------------------------------------------------------------------- 1 | // This tool converts an aiff file into an identical wav file and stores 2 | // it in the same folder as the source. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/user" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/go-audio/aiff" 15 | "github.com/go-audio/audio" 16 | "github.com/go-audio/wav" 17 | ) 18 | 19 | var ( 20 | flagPath = flag.String("path", "", "The path to the wav file to convert to aiff") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | if *flagPath == "" { 26 | fmt.Println("You must set the -path flag") 27 | os.Exit(1) 28 | } 29 | 30 | usr, err := user.Current() 31 | if err != nil { 32 | log.Println("Failed to get the user home directory") 33 | os.Exit(1) 34 | } 35 | 36 | sourcePath := *flagPath 37 | if sourcePath[:2] == "~/" { 38 | sourcePath = strings.Replace(sourcePath, "~", usr.HomeDir, 1) 39 | } 40 | 41 | f, err := os.Open(*flagPath) 42 | if err != nil { 43 | fmt.Println("Invalid path", *flagPath, err) 44 | os.Exit(1) 45 | } 46 | defer f.Close() 47 | 48 | d := wav.NewDecoder(f) 49 | if !d.IsValidFile() { 50 | fmt.Println("invalid WAV file") 51 | os.Exit(1) 52 | } 53 | 54 | outPath := sourcePath[:len(sourcePath)-len(filepath.Ext(sourcePath))] + ".aif" 55 | of, err := os.Create(outPath) 56 | if err != nil { 57 | fmt.Println("Failed to create", outPath) 58 | panic(err) 59 | } 60 | defer of.Close() 61 | 62 | e := aiff.NewEncoder(of, int(d.SampleRate), int(d.BitDepth), int(d.NumChans)) 63 | 64 | format := &audio.Format{ 65 | NumChannels: int(d.NumChans), 66 | SampleRate: int(d.SampleRate), 67 | } 68 | 69 | bufferSize := 1000000 70 | buf := &audio.IntBuffer{Data: make([]int, bufferSize), Format: format} 71 | var n int 72 | for err == nil { 73 | n, err = d.PCMBuffer(buf) 74 | if err != nil { 75 | break 76 | } 77 | if n == 0 { 78 | break 79 | } 80 | if n != len(buf.Data) { 81 | buf.Data = buf.Data[:n] 82 | } 83 | if err := e.Write(buf); err != nil { 84 | panic(err) 85 | } 86 | } 87 | 88 | if err := e.Close(); err != nil { 89 | panic(err) 90 | } 91 | fmt.Printf("Wav file converted to %s\n", outPath) 92 | } 93 | -------------------------------------------------------------------------------- /cue_chunk.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | "github.com/go-audio/riff" 9 | ) 10 | 11 | // CuePoint defines an offset which marks a noteworthy sections of the audio 12 | // content. For example, the beginning and end of a verse in a song may have cue 13 | // points to make them easier to find. 14 | type CuePoint struct { 15 | // ID is the unique identifier of the cue point 16 | ID [4]byte 17 | // Play position specifies the sample offset associated with the cue point 18 | // in terms of the sample's position in the final stream of samples 19 | // generated by the play list. If a play list chunk is 20 | // specified, the position value is equal to the sample number at which this 21 | // cue point will occur during playback of the entire play list as defined 22 | // by the play list's order. If no play list chunk is specified this value 23 | // should be 0. 24 | Position uint32 25 | // DataChunkID - This value specifies the four byte ID used by the chunk 26 | // containing the sample that corresponds to this cue point. A Wave file 27 | // with no play list is always "data". A Wave file with a play list 28 | // containing both sample data and silence may be either "data" or "slnt". 29 | DataChunkID [4]byte 30 | // ChunkStart specifies the byte offset into the Wave List Chunk of the 31 | // chunk containing the sample that corresponds to this cue point. This is 32 | // the same chunk described by the Data Chunk ID value. If no Wave List 33 | // Chunk exists in the Wave file, this value is 0. If a Wave List Chunk 34 | // exists, this is the offset into the "wavl" chunk. The first chunk in the 35 | // Wave List Chunk would be specified with a value of 0. 36 | ChunkStart uint32 37 | // BlockStart specifies the byte offset into the "data" or "slnt" Chunk to 38 | // the start of the block containing the sample. The start of a block is 39 | // defined as the first byte in uncompressed PCM wave data or the last byte 40 | // in compressed wave data where decompression can begin to find the value 41 | // of the corresponding sample value. 42 | BlockStart uint32 43 | // SampleOffset specifies an offset into the block (specified by Block 44 | // Start) for the sample that corresponds to the cue point. In uncompressed 45 | // PCM waveform data, this is simply the byte offset into the "data" chunk. 46 | // In compressed waveform data, this value is equal to the number of samples 47 | // (may or may not be bytes) from the Block Start to the sample that 48 | // corresponds to the cue point. 49 | SampleOffset uint32 50 | } 51 | 52 | // DecodeCueChunk decodes the optional cue chunk and extracts cue points. 53 | func DecodeCueChunk(d *Decoder, ch *riff.Chunk) error { 54 | if ch == nil { 55 | return fmt.Errorf("can't decode a nil chunk") 56 | } 57 | if d == nil { 58 | return fmt.Errorf("nil decoder") 59 | } 60 | if ch.ID == CIDCue { 61 | // read the entire chunk in memory 62 | buf := make([]byte, ch.Size) 63 | var err error 64 | if _, err = ch.Read(buf); err != nil { 65 | return fmt.Errorf("failed to read the CUE chunk - %w", err) 66 | } 67 | r := bytes.NewReader(buf) 68 | var nbrCues uint32 69 | if err := binary.Read(r, binary.LittleEndian, &nbrCues); err != nil { 70 | return fmt.Errorf("failed to read the number of cues - %w", err) 71 | } 72 | if nbrCues > 0 { 73 | if d.Metadata == nil { 74 | d.Metadata = &Metadata{} 75 | } 76 | d.Metadata.CuePoints = []*CuePoint{} 77 | scratch := make([]byte, 4) 78 | for i := uint32(0); i < nbrCues; i++ { 79 | c := &CuePoint{} 80 | if _, err = r.Read(scratch); err != nil { 81 | return fmt.Errorf("failed to read the cue point ID") 82 | } 83 | copy(c.ID[:], scratch[:4]) 84 | if err := binary.Read(r, binary.LittleEndian, &c.Position); err != nil { 85 | return err 86 | } 87 | if _, err = r.Read(scratch); err != nil { 88 | return fmt.Errorf("failed to read the data chunk id") 89 | } 90 | copy(c.DataChunkID[:], scratch[:4]) 91 | if err := binary.Read(r, binary.LittleEndian, &c.ChunkStart); err != nil { 92 | return err 93 | } 94 | if err := binary.Read(r, binary.LittleEndian, &c.BlockStart); err != nil { 95 | return err 96 | } 97 | if err := binary.Read(r, binary.LittleEndian, &c.SampleOffset); err != nil { 98 | return err 99 | } 100 | d.Metadata.CuePoints = append(d.Metadata.CuePoints, c) 101 | } 102 | } 103 | 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "time" 10 | 11 | "github.com/go-audio/audio" 12 | "github.com/go-audio/riff" 13 | ) 14 | 15 | var ( 16 | // CIDList is the chunk ID for a LIST chunk 17 | CIDList = [4]byte{'L', 'I', 'S', 'T'} 18 | // CIDSmpl is the chunk ID for a smpl chunk 19 | CIDSmpl = [4]byte{'s', 'm', 'p', 'l'} 20 | // CIDINFO is the chunk ID for an INFO chunk 21 | CIDInfo = []byte{'I', 'N', 'F', 'O'} 22 | // CIDCue is the chunk ID for the cue chunk 23 | CIDCue = [4]byte{'c', 'u', 'e', 0x20} 24 | ) 25 | 26 | // Decoder handles the decoding of wav files. 27 | type Decoder struct { 28 | r io.ReadSeeker 29 | parser *riff.Parser 30 | 31 | NumChans uint16 32 | BitDepth uint16 33 | SampleRate uint32 34 | 35 | AvgBytesPerSec uint32 36 | WavAudioFormat uint16 37 | 38 | err error 39 | PCMSize int 40 | pcmDataAccessed bool 41 | // pcmChunk is available so we can use the LimitReader 42 | PCMChunk *riff.Chunk 43 | // Metadata for the current file 44 | Metadata *Metadata 45 | } 46 | 47 | // NewDecoder creates a decoder for the passed wav reader. 48 | // Note that the reader doesn't get rewinded as the container is processed. 49 | func NewDecoder(r io.ReadSeeker) *Decoder { 50 | return &Decoder{ 51 | r: r, 52 | parser: riff.New(r), 53 | } 54 | } 55 | 56 | // Seek provides access to the cursor position in the PCM data 57 | func (d *Decoder) Seek(offset int64, whence int) (int64, error) { 58 | return d.r.Seek(offset, whence) 59 | } 60 | 61 | // Rewind allows the decoder to be rewound to the beginning of the PCM data. 62 | // This is useful if you want to keep on decoding the same file in a loop. 63 | func (d *Decoder) Rewind() error { 64 | 65 | _, err := d.r.Seek(0, io.SeekStart) 66 | if err != nil { 67 | return fmt.Errorf("failed to seek back to the start %w", err) 68 | } 69 | // we have to user a new parser since it's read only and can't be seeked 70 | d.parser = riff.New(d.r) 71 | d.pcmDataAccessed = false 72 | d.PCMChunk = nil 73 | d.err = nil 74 | d.NumChans = 0 75 | err = d.FwdToPCM() 76 | if err != nil { 77 | return fmt.Errorf("failed to seek to the PCM data: %w", err) 78 | } 79 | return nil 80 | } 81 | 82 | // SampleBitDepth returns the bit depth encoding of each sample. 83 | func (d *Decoder) SampleBitDepth() int32 { 84 | if d == nil { 85 | return 0 86 | } 87 | return int32(d.BitDepth) 88 | } 89 | 90 | // PCMLen returns the total number of bytes in the PCM data chunk 91 | func (d *Decoder) PCMLen() int64 { 92 | if d == nil { 93 | return 0 94 | } 95 | return int64(d.PCMSize) 96 | } 97 | 98 | // Err returns the first non-EOF error that was encountered by the Decoder. 99 | func (d *Decoder) Err() error { 100 | if errors.Is(d.err, io.EOF) { 101 | return nil 102 | } 103 | return d.err 104 | } 105 | 106 | // EOF returns positively if the underlying reader reached the end of file. 107 | func (d *Decoder) EOF() bool { 108 | if d == nil || errors.Is(d.err, io.EOF) { 109 | return true 110 | } 111 | return false 112 | } 113 | 114 | // IsValidFile verifies that the file is valid/readable. 115 | func (d *Decoder) IsValidFile() bool { 116 | d.err = d.readHeaders() 117 | if d.err != nil { 118 | return false 119 | } 120 | if d.NumChans < 1 { 121 | return false 122 | } 123 | if d.BitDepth < 8 { 124 | return false 125 | } 126 | if d, err := d.Duration(); err != nil || d <= 0 { 127 | return false 128 | } 129 | 130 | return true 131 | } 132 | 133 | // ReadInfo reads the underlying reader until the comm header is parsed. 134 | // This method is safe to call multiple times. 135 | func (d *Decoder) ReadInfo() { 136 | d.err = d.readHeaders() 137 | } 138 | 139 | // ReadMetadata parses the file for extra metadata such as the INFO list chunk. 140 | // The entire file will be read and should be rewinded if more data must be 141 | // accessed. 142 | func (d *Decoder) ReadMetadata() { 143 | if d.Metadata != nil { 144 | return 145 | } 146 | d.ReadInfo() 147 | if d.Err() != nil || d.Metadata != nil { 148 | return 149 | } 150 | var ( 151 | chunk *riff.Chunk 152 | err error 153 | ) 154 | for err == nil { 155 | chunk, err = d.parser.NextChunk() 156 | if err != nil { 157 | break 158 | } 159 | 160 | switch chunk.ID { 161 | case CIDList: 162 | if err = DecodeListChunk(d, chunk); err != nil { 163 | if !errors.Is(err, io.EOF) { 164 | d.err = err 165 | } 166 | } 167 | if d.Metadata != nil && d.Metadata.SamplerInfo != nil { 168 | // we got everything we were looking for 169 | break 170 | } 171 | case CIDSmpl: 172 | if err = DecodeSamplerChunk(d, chunk); err != nil { 173 | if !errors.Is(err, io.EOF) { 174 | d.err = err 175 | } 176 | } 177 | case CIDCue: 178 | if err = DecodeCueChunk(d, chunk); err != nil { 179 | if !errors.Is(err, io.EOF) { 180 | d.err = err 181 | } 182 | } 183 | default: 184 | // fmt.Println(string(chunk.ID[:])) 185 | chunk.Drain() 186 | } 187 | } 188 | 189 | } 190 | 191 | // FwdToPCM forwards the underlying reader until the start of the PCM chunk. 192 | // If the PCM chunk was already read, no data will be found (you need to rewind). 193 | func (d *Decoder) FwdToPCM() error { 194 | if d == nil { 195 | return fmt.Errorf("PCM data not found") 196 | } 197 | d.err = d.readHeaders() 198 | if d.err != nil { 199 | return nil 200 | } 201 | 202 | var chunk *riff.Chunk 203 | for d.err == nil { 204 | chunk, d.err = d.NextChunk() 205 | if d.err != nil { 206 | return d.err 207 | } 208 | if chunk.ID == riff.DataFormatID { 209 | d.PCMSize = chunk.Size 210 | d.PCMChunk = chunk 211 | break 212 | } 213 | if chunk.ID == CIDList { 214 | DecodeListChunk(d, chunk) 215 | } 216 | chunk.Drain() 217 | } 218 | if chunk == nil { 219 | return fmt.Errorf("PCM data not found") 220 | } 221 | d.pcmDataAccessed = true 222 | 223 | return nil 224 | } 225 | 226 | // WasPCMAccessed returns positively if the PCM data was previously accessed. 227 | func (d *Decoder) WasPCMAccessed() bool { 228 | if d == nil { 229 | return false 230 | } 231 | return d.pcmDataAccessed 232 | } 233 | 234 | // FullPCMBuffer is an inefficient way to access all the PCM data contained in the 235 | // audio container. The entire PCM data is held in memory. 236 | // Consider using PCMBuffer() instead. 237 | func (d *Decoder) FullPCMBuffer() (*audio.IntBuffer, error) { 238 | if !d.WasPCMAccessed() { 239 | err := d.FwdToPCM() 240 | if err != nil { 241 | return nil, d.err 242 | } 243 | } 244 | if d.PCMChunk == nil { 245 | return nil, errors.New("PCM chunk not found") 246 | } 247 | format := &audio.Format{ 248 | NumChannels: int(d.NumChans), 249 | SampleRate: int(d.SampleRate), 250 | } 251 | 252 | buf := &audio.IntBuffer{Data: make([]int, 4096), Format: format, SourceBitDepth: int(d.BitDepth)} 253 | bytesPerSample := (d.BitDepth-1)/8 + 1 254 | sampleBufData := make([]byte, bytesPerSample) 255 | decodeF, err := sampleDecodeFunc(int(d.BitDepth)) 256 | if err != nil { 257 | return nil, fmt.Errorf("could not get sample decode func %w", err) 258 | } 259 | 260 | i := 0 261 | for err == nil { 262 | buf.Data[i], err = decodeF(d.PCMChunk, sampleBufData) 263 | if err != nil { 264 | break 265 | } 266 | i++ 267 | // grow the underlying slice if needed 268 | if i == len(buf.Data) { 269 | buf.Data = append(buf.Data, make([]int, 4096)...) 270 | } 271 | } 272 | buf.Data = buf.Data[:i] 273 | 274 | if errors.Is(err, io.EOF) { 275 | err = nil 276 | } 277 | 278 | return buf, err 279 | } 280 | 281 | // PCMBuffer populates the passed PCM buffer 282 | func (d *Decoder) PCMBuffer(buf *audio.IntBuffer) (n int, err error) { 283 | if buf == nil { 284 | return 0, nil 285 | } 286 | 287 | if !d.pcmDataAccessed { 288 | err := d.FwdToPCM() 289 | if err != nil { 290 | return 0, d.err 291 | } 292 | } 293 | if d.PCMChunk == nil { 294 | return 0, ErrPCMChunkNotFound 295 | } 296 | 297 | format := &audio.Format{ 298 | NumChannels: int(d.NumChans), 299 | SampleRate: int(d.SampleRate), 300 | } 301 | 302 | buf.SourceBitDepth = int(d.BitDepth) 303 | decodeF, err := sampleDecodeFunc(int(d.BitDepth)) 304 | if err != nil { 305 | return 0, fmt.Errorf("could not get sample decode func %w", err) 306 | } 307 | 308 | bPerSample := bytesPerSample(int(d.BitDepth)) 309 | // populate a file buffer to avoid multiple very small reads 310 | // we need to cap the buffer size to not be bigger than the pcm chunk. 311 | size := len(buf.Data) * bPerSample 312 | tmpBuf := make([]byte, size) 313 | var m int 314 | m, err = d.PCMChunk.R.Read(tmpBuf) 315 | if err != nil { 316 | if errors.Is(err, io.EOF) { 317 | return m, nil 318 | } 319 | return m, err 320 | } 321 | if m == 0 { 322 | return m, nil 323 | } 324 | bufR := bytes.NewReader(tmpBuf[:m]) 325 | sampleBuf := make([]byte, bPerSample, bPerSample) 326 | var misaligned bool 327 | if m%bPerSample > 0 { 328 | misaligned = true 329 | } 330 | 331 | // Note that we populate the buffer even if the 332 | // size of the buffer doesn't fit an even number of frames. 333 | for n = 0; n < len(buf.Data); n++ { 334 | buf.Data[n], err = decodeF(bufR, sampleBuf) 335 | if err != nil { 336 | // the last sample isn't a full sample but just padding. 337 | if misaligned { 338 | n-- 339 | } 340 | break 341 | } 342 | } 343 | buf.Format = format 344 | if errors.Is(err, io.EOF) { 345 | err = nil 346 | } 347 | 348 | return n, err 349 | } 350 | 351 | // Format returns the audio format of the decoded content. 352 | func (d *Decoder) Format() *audio.Format { 353 | if d == nil { 354 | return nil 355 | } 356 | return &audio.Format{ 357 | NumChannels: int(d.NumChans), 358 | SampleRate: int(d.SampleRate), 359 | } 360 | } 361 | 362 | // NextChunk returns the next available chunk 363 | func (d *Decoder) NextChunk() (*riff.Chunk, error) { 364 | if d.err = d.readHeaders(); d.err != nil { 365 | d.err = fmt.Errorf("failed to read header - %v", d.err) 366 | return nil, d.err 367 | } 368 | 369 | var ( 370 | id [4]byte 371 | size uint32 372 | ) 373 | 374 | id, size, d.err = d.parser.IDnSize() 375 | if d.err != nil { 376 | d.err = fmt.Errorf("error reading chunk header - %v", d.err) 377 | return nil, d.err 378 | } 379 | 380 | // TODO: any reason we don't use d.parser.NextChunk (riff.NextChunk) here? 381 | // It correctly handles the misaligned chunk. 382 | 383 | // TODO: copied over from riff.parser.NextChunk 384 | // all RIFF chunks (including WAVE "data" chunks) must be word aligned. 385 | // If the data uses an odd number of bytes, a padding byte with a value of zero must be placed at the end of the sample data. 386 | // The "data" chunk header's size should not include this byte. 387 | if size%2 == 1 { 388 | size++ 389 | } 390 | 391 | c := &riff.Chunk{ 392 | ID: id, 393 | Size: int(size), 394 | R: io.LimitReader(d.r, int64(size)), 395 | } 396 | return c, d.err 397 | } 398 | 399 | // Duration returns the time duration for the current audio container 400 | func (d *Decoder) Duration() (time.Duration, error) { 401 | if d == nil || d.parser == nil { 402 | return 0, errors.New("can't calculate the duration of a nil pointer") 403 | } 404 | return d.parser.Duration() 405 | } 406 | 407 | // String implements the Stringer interface. 408 | func (d *Decoder) String() string { 409 | return d.parser.String() 410 | } 411 | 412 | // readHeaders is safe to call multiple times 413 | func (d *Decoder) readHeaders() error { 414 | if d == nil || d.NumChans > 0 { 415 | return nil 416 | } 417 | 418 | id, size, err := d.parser.IDnSize() 419 | if err != nil { 420 | return err 421 | } 422 | d.parser.ID = id 423 | if d.parser.ID != riff.RiffID { 424 | return fmt.Errorf("%s - %s", d.parser.ID, riff.ErrFmtNotSupported) 425 | } 426 | d.parser.Size = size 427 | if err := binary.Read(d.r, binary.BigEndian, &d.parser.Format); err != nil { 428 | return err 429 | } 430 | 431 | var chunk *riff.Chunk 432 | var rewindBytes int64 433 | 434 | for err == nil { 435 | chunk, err = d.parser.NextChunk() 436 | if err != nil { 437 | break 438 | } 439 | 440 | if chunk.ID == riff.FmtID { 441 | chunk.DecodeWavHeader(d.parser) 442 | d.NumChans = d.parser.NumChannels 443 | d.BitDepth = d.parser.BitsPerSample 444 | d.SampleRate = d.parser.SampleRate 445 | d.WavAudioFormat = d.parser.WavAudioFormat 446 | d.AvgBytesPerSec = d.parser.AvgBytesPerSec 447 | 448 | if rewindBytes > 0 { 449 | d.r.Seek(-(rewindBytes + int64(chunk.Size) + 8), 1) 450 | } 451 | break 452 | } else if chunk.ID == CIDList { 453 | // The list chunk can be in the header or footer 454 | // because so many players don't support that chunk properly 455 | // it is recommended to have it at the end of the file. 456 | DecodeListChunk(d, chunk) 457 | // unexpected chunk order, might be a bext chunk 458 | rewindBytes += int64(chunk.Size) + 8 459 | } else if chunk.ID == CIDSmpl { 460 | DecodeSamplerChunk(d, chunk) 461 | rewindBytes += int64(chunk.Size) + 8 462 | } else { 463 | // unexpected chunk order, might be a bext chunk 464 | rewindBytes += int64(chunk.Size) + 8 465 | // drain the chunk 466 | io.CopyN(io.Discard, d.r, int64(chunk.Size)) 467 | } 468 | } 469 | 470 | return d.err 471 | } 472 | 473 | func bytesPerSample(bitDepth int) int { 474 | return bitDepth / 8 475 | } 476 | 477 | // sampleDecodeFunc returns a function that can be used to convert 478 | // a byte range into an int value based on the amount of bits used per sample. 479 | // Note that 8bit samples are unsigned, all other values are signed. 480 | func sampleDecodeFunc(bitsPerSample int) (func(io.Reader, []byte) (int, error), error) { 481 | // NOTE: WAV PCM data is stored using little-endian 482 | switch bitsPerSample { 483 | case 8: 484 | // 8bit values are unsigned 485 | return func(r io.Reader, buf []byte) (int, error) { 486 | _, err := r.Read(buf[:1]) 487 | return int(buf[0]), err 488 | }, nil 489 | case 16: 490 | return func(r io.Reader, buf []byte) (int, error) { 491 | _, err := r.Read(buf[:2]) 492 | return int(int16(binary.LittleEndian.Uint16(buf[:2]))), err 493 | }, nil 494 | case 24: 495 | // -34,359,738,367 (0x7FFFFF) to 34,359,738,368 (0x800000) 496 | return func(r io.Reader, buf []byte) (int, error) { 497 | _, err := r.Read(buf[:3]) 498 | if err != nil { 499 | return 0, err 500 | } 501 | return int(audio.Int24LETo32(buf[:3])), nil 502 | }, nil 503 | case 32: 504 | return func(r io.Reader, buf []byte) (int, error) { 505 | _, err := r.Read(buf[:4]) 506 | return int(int32(binary.LittleEndian.Uint32(buf[:4]))), err 507 | }, nil 508 | default: 509 | return nil, fmt.Errorf("unhandled byte depth:%d", bitsPerSample) 510 | } 511 | } 512 | 513 | // sampleDecodeFloat64Func returns a function that can be used to convert 514 | // a byte range into a float64 value based on the amount of bits used per sample. 515 | func sampleFloat64DecodeFunc(bitsPerSample int) (func([]byte) float64, error) { 516 | bytesPerSample := bitsPerSample / 8 517 | switch bytesPerSample { 518 | case 1: 519 | // 8bit values are unsigned 520 | return func(s []byte) float64 { 521 | return float64(uint8(s[0])) 522 | }, nil 523 | case 2: 524 | return func(s []byte) float64 { 525 | return float64(int(s[0]) + int(s[1])<<8) 526 | }, nil 527 | case 3: 528 | return func(s []byte) float64 { 529 | var output int32 530 | output |= int32(s[2]) << 0 531 | output |= int32(s[1]) << 8 532 | output |= int32(s[0]) << 16 533 | return float64(output) 534 | }, nil 535 | case 4: 536 | // TODO: fix the float64 conversion (current int implementation) 537 | return func(s []byte) float64 { 538 | return float64(int(s[0]) + int(s[1])<<8 + int(s[2])<<16 + int(s[3])<<24) 539 | }, nil 540 | default: 541 | return nil, fmt.Errorf("unhandled byte depth:%d", bitsPerSample) 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-audio/audio" 11 | ) 12 | 13 | func TestDecoderSeek(t *testing.T) { 14 | f, err := os.Open("fixtures/bass.wav") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer f.Close() 19 | d := NewDecoder(f) 20 | // Move read cursor to the middle of the file 21 | // Using whence=0 should be os.SEEK_SET for go<=1.6.x else io.SeekStart 22 | cur, err := d.Seek(d.PCMLen()/2, 0) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if cur != d.PCMLen()/2 { 27 | t.Fatal("Read cursor no in the expected position") 28 | } 29 | } 30 | 31 | func TestDecoderRewind(t *testing.T) { 32 | f, err := os.Open("fixtures/bass.wav") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer f.Close() 37 | d := NewDecoder(f) 38 | d.ReadInfo() 39 | buf := &audio.IntBuffer{Format: d.Format(), Data: make([]int, 512)} 40 | n, err := d.PCMBuffer(buf) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if n != 512 { 45 | t.Fatalf("expected to read 512 samples but got %d", n) 46 | } 47 | if err := d.Rewind(); err != nil { 48 | t.Fatal(err) 49 | } 50 | newBuf := &audio.IntBuffer{Format: d.Format(), Data: make([]int, 512)} 51 | n, err = d.PCMBuffer(newBuf) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if n != 512 { 56 | t.Fatalf("expected to read 512 samples but got %d", n) 57 | } 58 | if !reflect.DeepEqual(buf.Data, newBuf.Data) { 59 | t.Fatal("expected to read the same data after rewinding") 60 | } 61 | } 62 | 63 | func TestDecoder_Duration(t *testing.T) { 64 | testCases := []struct { 65 | in string 66 | duration time.Duration 67 | }{ 68 | {"fixtures/kick.wav", time.Duration(204172335 * time.Nanosecond)}, 69 | } 70 | 71 | for _, tc := range testCases { 72 | f, err := os.Open(tc.in) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | dur, err := NewDecoder(f).Duration() 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if err := f.Close(); err != nil { 81 | t.Fatal(err) 82 | } 83 | if dur != tc.duration { 84 | t.Fatalf("expected duration to be: %s but was %s", tc.duration, dur) 85 | } 86 | } 87 | 88 | } 89 | 90 | func TestDecoder_IsValidFile(t *testing.T) { 91 | testCases := []struct { 92 | in string 93 | isValid bool 94 | }{ 95 | {"fixtures/kick.wav", true}, 96 | {"fixtures/bass.wav", true}, 97 | {"fixtures/dirty-kick-24b441k.wav", true}, 98 | {"fixtures/sample.avi", false}, 99 | {"fixtures/bloop.aif", false}, 100 | {"fixtures/bwf.wav", true}, 101 | } 102 | 103 | for _, tc := range testCases { 104 | f, err := os.Open(tc.in) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | defer f.Close() 109 | d := NewDecoder(f) 110 | if d.IsValidFile() != tc.isValid { 111 | t.Fatalf("validation of the wav files doesn't match expected %t, got %t", tc.isValid, d.IsValidFile()) 112 | } 113 | } 114 | 115 | } 116 | 117 | func TestReadContent(t *testing.T) { 118 | testCases := []struct { 119 | input string 120 | total int64 121 | err error 122 | }{ 123 | {"fixtures/kick.wav", 180896, nil}, 124 | {"fixtures/bwf.wav", 1003870765, nil}, 125 | } 126 | 127 | for _, tc := range testCases { 128 | t.Run(tc.input, func(t *testing.T) { 129 | r, err := os.Open(tc.input) 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | defer r.Close() 134 | 135 | d := NewDecoder(r) 136 | total, err := totaledDecoder(d) 137 | if err != tc.err { 138 | t.Errorf("Expected err to be %v but got %v", tc.err, err) 139 | } 140 | if total != tc.total { 141 | t.Errorf("Expected total to be %d but got %d", tc.total, total) 142 | } 143 | }) 144 | } 145 | 146 | } 147 | 148 | func TestDecoder_Attributes(t *testing.T) { 149 | testCases := []struct { 150 | in string 151 | numChannels int 152 | sampleRate int 153 | avgBytesPerSec int 154 | bitDepth int 155 | }{ 156 | {in: "fixtures/kick.wav", 157 | numChannels: 1, 158 | sampleRate: 22050, 159 | avgBytesPerSec: 44100, 160 | bitDepth: 16, 161 | }, 162 | } 163 | 164 | for _, tc := range testCases { 165 | f, err := os.Open(tc.in) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | d := NewDecoder(f) 170 | d.ReadInfo() 171 | f.Close() 172 | if int(d.NumChans) != tc.numChannels { 173 | t.Fatalf("expected info to have %d channels but it has %d", tc.numChannels, d.NumChans) 174 | } 175 | if int(d.SampleRate) != tc.sampleRate { 176 | t.Fatalf("expected info to have a sample rate of %d but it has %d", tc.sampleRate, d.SampleRate) 177 | } 178 | if int(d.AvgBytesPerSec) != tc.avgBytesPerSec { 179 | t.Fatalf("expected info to have %d avg bytes per sec but it has %d", tc.avgBytesPerSec, d.AvgBytesPerSec) 180 | } 181 | if int(d.BitDepth) != tc.bitDepth { 182 | t.Fatalf("expected info to have %d bits per sample but it has %d", tc.bitDepth, d.BitDepth) 183 | } 184 | } 185 | } 186 | 187 | func TestDecoderMisalignedInstChunk(t *testing.T) { 188 | f, err := os.Open("fixtures/misaligned-chunk.wav") 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | defer f.Close() 193 | 194 | d := NewDecoder(f) 195 | intBuf := make([]int, 255) 196 | buf := &audio.IntBuffer{Data: intBuf} 197 | if _, err := d.PCMBuffer(buf); err != nil { 198 | t.Fatal(err) 199 | } 200 | } 201 | 202 | func TestDecoder_PCMBuffer(t *testing.T) { 203 | testCases := []struct { 204 | input string 205 | desc string 206 | bitDepth int 207 | samples []int 208 | samplesAvailable int 209 | }{ 210 | {"fixtures/bass.wav", 211 | "bass.wav 2 ch, 44100 Hz, 24-bit little-endian signed integer", 212 | 24, 213 | []int{0, 0, 110, 103, 63, 58, -2915, -2756, 2330, 2209, 8443, 8009, -1199, -1062, -2373, -2101, -6344, -5771, -17792, -16537, -64843, -61110, -82618, -78260, -24782, -24011, 111633, 104295, 235773, 221196, 275505}, 214 | 47914, 215 | }, 216 | {"fixtures/kick-16b441k.wav", 217 | "kick-16b441k.wav 2 ch, 44100 Hz, '16-bit little-endian signed integer", 218 | 16, 219 | []int{0, 0, 0, 0, 0, 0, 3, 3, 28, 28, 130, 130, 436, 436, 1103, 1103, 2140, 2140, 3073, 3073, 2884, 2884, 760, 760, -2755, -2755, -5182, -5182, -3860, -3860, 1048, 1048, 5303, 5303, 3885, 3885, -3378, -3378, -9971, -9971, -8119, -8119, 2616, 2616, 13344, 13344, 13297, 13297, 553, 553, -15013, -15013, -20341, -20341, -10692, -10692, 6553, 6553, 18819, 18819, 18824, 18824, 8617, 8617, -4253, -4253, -13305, -13305, -16289, -16289, -13913, -13913, -7552, -7552, 1334, 1334, 10383, 10383, 16409, 16409, 16928, 16928, 11771, 11771, 3121, 3121, -5908, -5908, -12829, -12829, -16321, -16321, -15990, -15990, -12025, -12025, -5273, -5273, 2732, 2732, 10094, 10094, 15172, 15172, 17038, 17038, 15563, 15563, 11232, 11232, 4973, 4971, -2044, -2044, -8602, -8602, -13659, -13659, -16458, -16458, -16574, -16575, -14012, -14012, -9294, -9294, -3352, -3352, 2823, 2823, 8485, 8485, 13125, 13125, 16228, 16228, 17214, 17214, 15766, 15766, 12188, 12188, 7355, 7355, 2152, 2152, -2973, -2973, -7929, -7929, -12446, -12446, -15806, -15806, -17161, -17161, -16200, -16200, -13407, -13407, -9681, -9681, -5659, -5659, -1418, -1418, 3212, 3212, 8092, 8092, 12567, 12567, 15766, 15766, 17123, 17123, 16665, 16665, 14863, 14863, 12262, 12262, 9171, 9171, 5644, 5644, 1636, 1636, -2768, -2768, -7262, -7262, -11344, -11344, -14486, -14486, -16310, -16310, -16710, -16710, -15861, -15861, -14093, -14093, -11737, -11737, -8974, -8974, -5840, -5840, -2309, -2309, 1577, 1577, 5631, 5631, 9510, 9510, 12821, 12821, 15218, 15218, 16500, 16500, 16663, 16663, 15861, 15861, 14338, 14338, 12322, 12322, 9960, 9960}, 220 | 15564, 221 | }, 222 | {"fixtures/padded24b.wav", 223 | "24b padded wav file", 224 | 24, 225 | nil, 226 | 3713, 227 | }, 228 | {"fixtures/listChunkInHeader.wav", 229 | "LIST chunk before the PCM data", 230 | 24, 231 | nil, 232 | 30636, 233 | }, 234 | } 235 | 236 | for _, tc := range testCases { 237 | t.Run(tc.desc, func(t *testing.T) { 238 | path, _ := filepath.Abs(tc.input) 239 | f, err := os.Open(path) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | defer f.Close() 244 | d := NewDecoder(f) 245 | 246 | samples := []int{} 247 | intBuf := make([]int, 255) 248 | buf := &audio.IntBuffer{Data: intBuf} 249 | var samplesAvailable int 250 | var n int 251 | for err == nil { 252 | n, err = d.PCMBuffer(buf) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | if n == 0 { 257 | break 258 | } 259 | samplesAvailable += n 260 | samples = append(samples, buf.Data...) 261 | } 262 | if samplesAvailable != tc.samplesAvailable { 263 | t.Fatalf("expected %d samples available, got %d", tc.samplesAvailable, samplesAvailable) 264 | } 265 | 266 | if buf.SourceBitDepth != tc.bitDepth { 267 | t.Fatalf("expected source bit depth to be %d but got %d", tc.bitDepth, buf.SourceBitDepth) 268 | } 269 | 270 | // allow to test the first samples of the content 271 | if tc.samples != nil { 272 | for i, sample := range samples { 273 | if i >= len(tc.samples) { 274 | break 275 | } 276 | if sample != tc.samples[i] { 277 | t.Fatalf("Expected %d at position %d, but got %d", tc.samples[i], i, sample) 278 | } 279 | } 280 | } 281 | }) 282 | } 283 | } 284 | 285 | func TestDecoder_FullPCMBuffer(t *testing.T) { 286 | testCases := []struct { 287 | input string 288 | desc string 289 | samples []int 290 | numSamples int 291 | totalFrames int 292 | numChannels int 293 | sampleRate int 294 | bitDepth int 295 | }{ 296 | {"fixtures/bass.wav", 297 | "2 ch, 44100 Hz, 'lpcm' 24-bit little-endian signed integer", 298 | []int{0, 0, 110, 103, 63, 58, -2915, -2756, 2330, 2209, 8443, 8009, -1199, -1062, -2373, -2101, -6344, -5771, -17792, -16537, -64843, -61110, -82618, -78260, -24782, -24011, 111633, 104295, 235773, 221196, 275505}, 299 | 47914, 300 | 23957, 301 | 2, 302 | 44100, 303 | 24, 304 | }, 305 | {"fixtures/kick-16b441k.wav", 306 | "2 ch, 44100 Hz, 'lpcm' (0x0000000C) 16-bit little-endian signed integer", 307 | []int{0, 0, 0, 0, 0, 0, 3, 3, 28, 28, 130, 130, 436, 436, 1103, 1103, 2140, 2140, 3073, 3073, 2884, 2884, 760, 760, -2755, -2755, -5182, -5182, -3860, -3860, 1048, 1048, 5303, 5303, 3885, 3885, -3378, -3378, -9971, -9971, -8119, -8119, 2616, 2616, 13344, 13344, 13297, 13297, 553, 553, -15013, -15013, -20341, -20341, -10692, -10692, 6553, 6553, 18819, 18819, 18824, 18824, 8617, 8617, -4253, -4253, -13305, -13305, -16289, -16289, -13913, -13913, -7552, -7552, 1334, 1334, 10383, 10383, 16409, 16409, 16928, 16928, 11771, 11771, 3121, 3121, -5908, -5908, -12829, -12829, -16321, -16321, -15990, -15990, -12025, -12025, -5273, -5273, 2732, 2732, 10094, 10094, 15172, 15172, 17038, 17038, 15563, 15563, 11232, 11232, 4973, 4971, -2044, -2044, -8602, -8602, -13659, -13659, -16458, -16458, -16574, -16575, -14012, -14012, -9294, -9294, -3352, -3352, 2823, 2823, 8485, 8485, 13125, 13125, 16228, 16228, 17214, 17214, 15766, 15766, 12188, 12188, 7355, 7355, 2152, 2152, -2973, -2973, -7929, -7929, -12446, -12446, -15806, -15806, -17161, -17161, -16200, -16200, -13407, -13407, -9681, -9681, -5659, -5659, -1418, -1418, 3212, 3212, 8092, 8092, 12567, 12567, 15766, 15766, 17123, 17123, 16665, 16665, 14863, 14863, 12262, 12262, 9171, 9171, 5644, 5644, 1636, 1636, -2768, -2768, -7262, -7262, -11344, -11344, -14486, -14486, -16310, -16310, -16710, -16710, -15861, -15861, -14093, -14093, -11737, -11737, -8974, -8974, -5840, -5840, -2309, -2309, 1577, 1577, 5631, 5631, 9510, 9510, 12821, 12821, 15218, 15218, 16500, 16500, 16663, 16663, 15861, 15861, 14338, 14338, 12322, 12322, 9960, 9960}, 308 | 15564, 309 | 7782, 310 | 2, 311 | 44100, 312 | 16, 313 | }, 314 | { 315 | "fixtures/kick.wav", 316 | "1 ch, 22050 Hz, 'lpcm' 16-bit little-endian signed integer", 317 | []int{76, 75, 77, 73, 74, 69, 73, 68, 72, 66, 67, 71, 529, 1427, 2243, 2943, 3512, 3953, 4258, 4436, 4486, 4412, 4220, 3901, 3476, 2937, 2294, 1555, 709, -212, -1231, -2322}, 318 | 4484, 319 | 4484, 320 | 1, 321 | 22050, 322 | 16, 323 | }, 324 | } 325 | 326 | for i, tc := range testCases { 327 | t.Logf("%d - %s\n", i, tc.input) 328 | path, _ := filepath.Abs(tc.input) 329 | f, err := os.Open(path) 330 | if err != nil { 331 | t.Fatal(err) 332 | } 333 | defer f.Close() 334 | d := NewDecoder(f) 335 | buf, err := d.FullPCMBuffer() 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | if len(buf.Data) != tc.numSamples { 340 | t.Fatalf("the length of the buffer (%d) didn't match what we expected (%d)", len(buf.Data), tc.numSamples) 341 | } 342 | for i := 0; i < len(tc.samples); i++ { 343 | if buf.Data[i] != tc.samples[i] { 344 | t.Fatalf("Expected %d at position %d, but got %d", tc.samples[i], i, buf.Data[i]) 345 | } 346 | } 347 | if buf.Format.SampleRate != tc.sampleRate { 348 | t.Fatalf("expected samplerate to be %d but got %d", tc.sampleRate, buf.Format.SampleRate) 349 | } 350 | if buf.Format.NumChannels != tc.numChannels { 351 | t.Fatalf("expected channel number to be %d but got %d", tc.numChannels, buf.Format.NumChannels) 352 | } 353 | framesNbr := buf.NumFrames() 354 | if framesNbr != tc.totalFrames { 355 | t.Fatalf("Expected %d frames, got %d\n", tc.totalFrames, framesNbr) 356 | } 357 | } 358 | } 359 | 360 | func totaledDecoder(d *Decoder) (total int64, err error) { 361 | format := &audio.Format{ 362 | NumChannels: int(d.NumChans), 363 | SampleRate: int(d.SampleRate), 364 | } 365 | 366 | chunkSize := 4096 367 | bits := d.BitDepth 368 | buf := &audio.IntBuffer{Data: make([]int, chunkSize), Format: format} 369 | var n int 370 | 371 | for err == nil { 372 | n, err = d.PCMBuffer(buf) 373 | if err != nil { 374 | break 375 | } 376 | if n == 0 { 377 | break 378 | } 379 | for i, s := range buf.Data { 380 | // the buffer is longer than than the data we have, we are done 381 | if i == n { 382 | break 383 | } 384 | switch bits { 385 | case 16: 386 | total += int64(int32(int16(s))) 387 | default: 388 | total += int64(int32(s)) 389 | } 390 | } 391 | if n != chunkSize { 392 | break 393 | } 394 | } 395 | if err == nil { 396 | err = d.Err() 397 | } 398 | 399 | return total, err 400 | } 401 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "time" 11 | 12 | "github.com/go-audio/audio" 13 | "github.com/go-audio/riff" 14 | ) 15 | 16 | // Encoder encodes LPCM data into a wav containter. 17 | type Encoder struct { 18 | w io.WriteSeeker 19 | buf *bytes.Buffer 20 | 21 | SampleRate int 22 | BitDepth int 23 | NumChans int 24 | 25 | // A number indicating the WAVE format category of the file. The content of 26 | // the portion of the ‘fmt’ chunk, and the 27 | // interpretation of the waveform data, depend on this value. PCM = 1 (i.e. 28 | // Linear quantization) Values other than 1 indicate some form of 29 | // compression. 30 | WavAudioFormat int 31 | 32 | // Metadata contains metadata to inject in the file. 33 | Metadata *Metadata 34 | 35 | WrittenBytes int 36 | frames int 37 | pcmChunkStarted bool 38 | pcmChunkSizePos int 39 | wroteHeader bool // true if we've written the header out 40 | } 41 | 42 | // NewEncoder creates a new encoder to create a new wav file. 43 | // Don't forget to add Frames to the encoder before writing. 44 | func NewEncoder(w io.WriteSeeker, sampleRate, bitDepth, numChans, audioFormat int) *Encoder { 45 | return &Encoder{ 46 | w: w, 47 | buf: bytes.NewBuffer(make([]byte, 0, bytesNumFromDuration(time.Minute, sampleRate, bitDepth)*numChans)), 48 | SampleRate: sampleRate, 49 | BitDepth: bitDepth, 50 | NumChans: numChans, 51 | WavAudioFormat: audioFormat, 52 | } 53 | } 54 | 55 | // AddLE serializes and adds the passed value using little endian 56 | func (e *Encoder) AddLE(src interface{}) error { 57 | e.WrittenBytes += binary.Size(src) 58 | return binary.Write(e.w, binary.LittleEndian, src) 59 | } 60 | 61 | // AddBE serializes and adds the passed value using big endian 62 | func (e *Encoder) AddBE(src interface{}) error { 63 | e.WrittenBytes += binary.Size(src) 64 | return binary.Write(e.w, binary.BigEndian, src) 65 | } 66 | 67 | func (e *Encoder) addBuffer(buf *audio.IntBuffer) error { 68 | if buf == nil { 69 | return fmt.Errorf("can't add a nil buffer") 70 | } 71 | 72 | frameCount := buf.NumFrames() 73 | // performance tweak: setup a buffer so we don't do too many writes 74 | var err error 75 | for i := 0; i < frameCount; i++ { 76 | for j := 0; j < buf.Format.NumChannels; j++ { 77 | v := buf.Data[i*buf.Format.NumChannels+j] 78 | switch e.BitDepth { 79 | case 8: 80 | if err = binary.Write(e.buf, binary.LittleEndian, uint8(v)); err != nil { 81 | return err 82 | } 83 | case 16: 84 | if err = binary.Write(e.buf, binary.LittleEndian, int16(v)); err != nil { 85 | return err 86 | } 87 | case 24: 88 | if err = binary.Write(e.buf, binary.LittleEndian, audio.Int32toInt24LEBytes(int32(v))); err != nil { 89 | return err 90 | } 91 | case 32: 92 | if err = binary.Write(e.buf, binary.LittleEndian, int32(v)); err != nil { 93 | return err 94 | } 95 | default: 96 | return fmt.Errorf("can't add frames of bit size %d", e.BitDepth) 97 | } 98 | } 99 | e.frames++ 100 | } 101 | if n, err := e.w.Write(e.buf.Bytes()); err != nil { 102 | e.WrittenBytes += n 103 | return err 104 | } 105 | e.WrittenBytes += e.buf.Len() 106 | e.buf.Reset() 107 | 108 | return nil 109 | } 110 | 111 | func (e *Encoder) writeHeader() error { 112 | if e.wroteHeader { 113 | return errors.New("already wrote header") 114 | } 115 | e.wroteHeader = true 116 | if e == nil { 117 | return fmt.Errorf("can't write a nil encoder") 118 | } 119 | if e.w == nil { 120 | return fmt.Errorf("can't write to a nil writer") 121 | } 122 | 123 | if e.WrittenBytes > 0 { 124 | return nil 125 | } 126 | 127 | // riff ID 128 | if err := e.AddLE(riff.RiffID); err != nil { 129 | return err 130 | } 131 | // file size uint32, to update later on. 132 | if err := e.AddLE(uint32(4294967295)); err != nil { 133 | return err 134 | } 135 | // wave headers 136 | if err := e.AddLE(riff.WavFormatID); err != nil { 137 | return err 138 | } 139 | // form 140 | if err := e.AddLE(riff.FmtID); err != nil { 141 | return err 142 | } 143 | // chunk size 144 | if err := e.AddLE(uint32(16)); err != nil { 145 | return err 146 | } 147 | // wave format 148 | if err := e.AddLE(uint16(e.WavAudioFormat)); err != nil { 149 | return err 150 | } 151 | // num channels 152 | if err := e.AddLE(uint16(e.NumChans)); err != nil { 153 | return fmt.Errorf("error encoding the number of channels - %w", err) 154 | } 155 | // samplerate 156 | if err := e.AddLE(uint32(e.SampleRate)); err != nil { 157 | return fmt.Errorf("error encoding the sample rate - %w", err) 158 | } 159 | blockAlign := e.NumChans * e.BitDepth / 8 160 | // avg bytes per sec 161 | if err := e.AddLE(uint32(e.SampleRate * blockAlign)); err != nil { 162 | return fmt.Errorf("error encoding the avg bytes per sec - %w", err) 163 | } 164 | // block align 165 | if err := e.AddLE(uint16(blockAlign)); err != nil { 166 | return err 167 | } 168 | // bits per sample 169 | if err := e.AddLE(uint16(e.BitDepth)); err != nil { 170 | return fmt.Errorf("error encoding bits per sample - %w", err) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // Write encodes and writes the passed buffer to the underlying writer. 177 | // Don't forget to Close() the encoder or the file won't be valid. 178 | func (e *Encoder) Write(buf *audio.IntBuffer) error { 179 | if !e.wroteHeader { 180 | if err := e.writeHeader(); err != nil { 181 | return err 182 | } 183 | } 184 | 185 | if !e.pcmChunkStarted { 186 | // sound header 187 | if err := e.AddLE(riff.DataFormatID); err != nil { 188 | return fmt.Errorf("error encoding sound header %w", err) 189 | } 190 | e.pcmChunkStarted = true 191 | 192 | // write a temporary chunksize 193 | e.pcmChunkSizePos = e.WrittenBytes 194 | if err := e.AddLE(uint32(4294967295)); err != nil { 195 | return fmt.Errorf("%w when writing wav data chunk size header", err) 196 | } 197 | } 198 | 199 | return e.addBuffer(buf) 200 | } 201 | 202 | // WriteFrame writes a single frame of data to the underlying writer. 203 | func (e *Encoder) WriteFrame(value interface{}) error { 204 | if !e.wroteHeader { 205 | e.writeHeader() 206 | } 207 | if !e.pcmChunkStarted { 208 | // sound header 209 | if err := e.AddLE(riff.DataFormatID); err != nil { 210 | return fmt.Errorf("error encoding sound header %w", err) 211 | } 212 | e.pcmChunkStarted = true 213 | 214 | // write a temporary chunksize 215 | e.pcmChunkSizePos = e.WrittenBytes 216 | if err := e.AddLE(uint32(4294967295)); err != nil { 217 | return fmt.Errorf("%w when writing wav data chunk size header", err) 218 | } 219 | } 220 | 221 | e.frames++ 222 | return e.AddLE(value) 223 | } 224 | 225 | func (e *Encoder) writeMetadata() error { 226 | chunkData := encodeInfoChunk(e) 227 | if err := e.AddBE(CIDList); err != nil { 228 | return fmt.Errorf("failed to write the LIST chunk ID: %w", err) 229 | } 230 | if err := e.AddLE(uint32(len(chunkData))); err != nil { 231 | return fmt.Errorf("failed to write the LIST chunk size: %w", err) 232 | } 233 | return e.AddBE(chunkData) 234 | } 235 | 236 | // Close flushes the content to disk, make sure the headers are up to date 237 | // Note that the underlying writer is NOT being closed. 238 | func (e *Encoder) Close() error { 239 | if e == nil || e.w == nil { 240 | return nil 241 | } 242 | 243 | // inject metadata at the end to not trip implementation not supporting 244 | // metadata chunks 245 | if e.Metadata != nil { 246 | if err := e.writeMetadata(); err != nil { 247 | return fmt.Errorf("failed to write metadata - %w", err) 248 | } 249 | } 250 | 251 | // go back and write total size in header 252 | if _, err := e.w.Seek(4, 0); err != nil { 253 | return err 254 | } 255 | if err := e.AddLE(uint32(e.WrittenBytes) - 8); err != nil { 256 | return fmt.Errorf("%w when writing the total written bytes", err) 257 | } 258 | 259 | // rewrite the audio chunk length header 260 | if e.pcmChunkSizePos > 0 { 261 | if _, err := e.w.Seek(int64(e.pcmChunkSizePos), 0); err != nil { 262 | return err 263 | } 264 | chunksize := uint32((int(e.BitDepth) / 8) * int(e.NumChans) * e.frames) 265 | if err := e.AddLE(uint32(chunksize)); err != nil { 266 | return fmt.Errorf("%w when writing wav data chunk size header", err) 267 | } 268 | } 269 | 270 | // jump back to the end of the file. 271 | if _, err := e.w.Seek(0, 2); err != nil { 272 | return err 273 | } 274 | switch e.w.(type) { 275 | case *os.File: 276 | return e.w.(*os.File).Sync() 277 | } 278 | return nil 279 | } 280 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | ) 8 | 9 | func TestEncoderRoundTrip(t *testing.T) { 10 | os.Mkdir("testOutput", 0777) 11 | testCases := []struct { 12 | in string 13 | out string 14 | metadata *Metadata 15 | desc string 16 | }{ 17 | {"fixtures/kick.wav", "testOutput/kick.wav", nil, "22050 Hz @ 16 bits, 1 channel(s), 44100 avg bytes/sec, duration: 204.172335ms"}, 18 | {"fixtures/kick-16b441k.wav", "testOutput/kick-16b441k.wav", nil, "2 ch, 44100 Hz, 'lpcm' 16-bit little-endian signed integer"}, 19 | {"fixtures/bass.wav", "testOutput/bass.wav", nil, "44100 Hz @ 24 bits, 2 channel(s), 264600 avg bytes/sec, duration: 543.378684ms"}, 20 | {"fixtures/8bit.wav", "testOutput/8bit.wav", &Metadata{ 21 | Artist: "Matt", Copyright: "copyleft", Comments: "A comment", CreationDate: "2017-12-12", Engineer: "Matt A", Technician: "Matt Aimonetti", 22 | Genre: "test", Keywords: "go code", Medium: "Virtual", Title: "Titre", Product: "go-audio", Subject: "wav codec", 23 | Software: "go-audio codec", Source: "Audacity generator", Location: "Los Angeles", TrackNbr: "42", 24 | }, "1 ch, 44100 Hz, 8-bit unsigned integer"}, 25 | {"fixtures/32bit.wav", "testOutput/32bit.wav", nil, "1 ch, 44100 Hz, 32-bit little-endian signed integer"}, 26 | } 27 | 28 | for _, tc := range testCases { 29 | t.Run(path.Base(tc.in), func(t *testing.T) { 30 | in, err := os.Open(tc.in) 31 | if err != nil { 32 | t.Fatalf("couldn't open %s %v", tc.in, err) 33 | } 34 | d := NewDecoder(in) 35 | buf, err := d.FullPCMBuffer() 36 | if err != nil { 37 | t.Fatalf("couldn't read buffer %s %v", tc.in, err) 38 | } 39 | 40 | in.Close() 41 | 42 | out, err := os.Create(tc.out) 43 | if err != nil { 44 | t.Fatalf("couldn't create %s %v", tc.out, err) 45 | } 46 | 47 | e := NewEncoder(out, 48 | buf.Format.SampleRate, 49 | int(d.BitDepth), 50 | buf.Format.NumChannels, 51 | int(d.WavAudioFormat)) 52 | if err = e.Write(buf); err != nil { 53 | t.Fatal(err) 54 | } 55 | if tc.metadata != nil { 56 | e.Metadata = tc.metadata 57 | } 58 | if err = e.Close(); err != nil { 59 | t.Fatal(err) 60 | } 61 | out.Close() 62 | 63 | nf, err := os.Open(tc.out) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | nd := NewDecoder(nf) 69 | nBuf, err := nd.FullPCMBuffer() 70 | if err != nil { 71 | t.Fatalf("couldn't extract the PCM from %s - %v", nf.Name(), err) 72 | } 73 | if tc.metadata != nil { 74 | nd.ReadMetadata() 75 | if nd.Metadata == nil { 76 | t.Errorf("expected some metadata, got a nil value") 77 | } 78 | if tc.metadata.Artist != nd.Metadata.Artist { 79 | t.Errorf("expected Artist to be %s, but was %s", tc.metadata.Artist, nd.Metadata.Artist) 80 | } 81 | if tc.metadata.Comments != nd.Metadata.Comments { 82 | t.Errorf("expected Comments to be %s, but was %s", tc.metadata.Comments, nd.Metadata.Comments) 83 | } 84 | if tc.metadata.Copyright != nd.Metadata.Copyright { 85 | t.Errorf("expected Copyright to be %s, but was %s", tc.metadata.Copyright, nd.Metadata.Copyright) 86 | } 87 | if tc.metadata.CreationDate != nd.Metadata.CreationDate { 88 | t.Errorf("expected CreationDate to be %s, but was %s", tc.metadata.CreationDate, nd.Metadata.CreationDate) 89 | } 90 | if tc.metadata.Engineer != nd.Metadata.Engineer { 91 | t.Errorf("expected Engineer to be %s, but was %s", tc.metadata.Engineer, nd.Metadata.Engineer) 92 | } 93 | if tc.metadata.Technician != nd.Metadata.Technician { 94 | t.Errorf("expected Technician to be %s, but was %s", tc.metadata.Technician, nd.Metadata.Technician) 95 | } 96 | if tc.metadata.Genre != nd.Metadata.Genre { 97 | t.Errorf("expected Genre to be %s, but was %s", tc.metadata.Genre, nd.Metadata.Genre) 98 | } 99 | if tc.metadata.Keywords != nd.Metadata.Keywords { 100 | t.Errorf("expected Keywords to be %s, but was %s", tc.metadata.Keywords, nd.Metadata.Keywords) 101 | } 102 | if tc.metadata.Medium != nd.Metadata.Medium { 103 | t.Errorf("expected Medium to be %s, but was %s", tc.metadata.Medium, nd.Metadata.Medium) 104 | } 105 | if tc.metadata.Title != nd.Metadata.Title { 106 | t.Errorf("expected Title to be %s, but was %s", tc.metadata.Title, nd.Metadata.Title) 107 | } 108 | if tc.metadata.Product != nd.Metadata.Product { 109 | t.Errorf("expected Product to be %s, but was %s", tc.metadata.Product, nd.Metadata.Product) 110 | } 111 | if tc.metadata.Subject != nd.Metadata.Subject { 112 | t.Errorf("expected Subject to be %s, but was %s", tc.metadata.Subject, nd.Metadata.Subject) 113 | } 114 | if tc.metadata.Software != nd.Metadata.Software { 115 | t.Errorf("expected Software to be %s, but was %s", tc.metadata.Software, nd.Metadata.Software) 116 | } 117 | if tc.metadata.Source != nd.Metadata.Source { 118 | t.Errorf("expected Source to be %s, but was %s", tc.metadata.Source, nd.Metadata.Source) 119 | } 120 | if tc.metadata.Location != nd.Metadata.Location { 121 | t.Errorf("expected Location to be %s, but was %s", tc.metadata.Location, nd.Metadata.Location) 122 | } 123 | if tc.metadata.TrackNbr != nd.Metadata.TrackNbr { 124 | t.Errorf("expected TrackNbr to be %s, but was %s", tc.metadata.TrackNbr, nd.Metadata.TrackNbr) 125 | } 126 | } 127 | 128 | nf.Close() 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | defer func() { 133 | if err := os.Remove(nf.Name()); err != nil { 134 | panic(err) 135 | } 136 | }() 137 | 138 | if nBuf.Format.SampleRate != buf.Format.SampleRate { 139 | t.Fatalf("sample rate didn't support roundtripping exp: %d, got: %d", buf.Format.SampleRate, nBuf.Format.SampleRate) 140 | } 141 | if nBuf.Format.NumChannels != buf.Format.NumChannels { 142 | t.Fatalf("the number of channels didn't support roundtripping exp: %d, got: %d", buf.Format.NumChannels, nBuf.Format.NumChannels) 143 | } 144 | if len(nBuf.Data) != len(buf.Data) { 145 | t.Fatalf("the reported number of frames didn't support roundtripping, exp: %d, got: %d", len(buf.Data), len(nBuf.Data)) 146 | } 147 | for i := 0; i < len(buf.Data); i++ { 148 | if buf.Data[i] != nBuf.Data[i] { 149 | max := len(buf.Data) 150 | if i+3 < max { 151 | max = i + 3 152 | } 153 | t.Fatalf("frame value at position %d: %d -> %d\ndidn't match new buffer position %d: %d -> %d", i, buf.Data[:i+1], buf.Data[i+1:max], i, nBuf.Data[:i+1], nBuf.Data[i+1:max]) 154 | } 155 | } 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func ExampleDecoder_Duration() { 10 | f, err := os.Open("fixtures/kick.wav") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer f.Close() 15 | dur, err := NewDecoder(f).Duration() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | fmt.Printf("%s duration: %s\n", f.Name(), dur) 20 | // Output: fixtures/kick.wav duration: 204.172335ms 21 | } 22 | 23 | func ExampleDecoder_IsValidFile() { 24 | f, err := os.Open("fixtures/kick.wav") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | defer f.Close() 29 | fmt.Printf("is this file valid: %t", NewDecoder(f).IsValidFile()) 30 | // Output: is this file valid: true 31 | } 32 | 33 | func ExampleEncoder_Write() { 34 | f, err := os.Open("fixtures/kick.wav") 35 | if err != nil { 36 | panic(fmt.Sprintf("couldn't open audio file - %v", err)) 37 | } 38 | 39 | // Decode the original audio file 40 | // and collect audio content and information. 41 | d := NewDecoder(f) 42 | buf, err := d.FullPCMBuffer() 43 | if err != nil { 44 | panic(err) 45 | } 46 | f.Close() 47 | fmt.Println("Old file ->", d) 48 | 49 | // Destination file 50 | out, err := os.Create("testOutput/kick.wav") 51 | if err != nil { 52 | panic(fmt.Sprintf("couldn't create output file - %v", err)) 53 | } 54 | 55 | // setup the encoder and write all the frames 56 | e := NewEncoder(out, 57 | buf.Format.SampleRate, 58 | int(d.BitDepth), 59 | buf.Format.NumChannels, 60 | int(d.WavAudioFormat)) 61 | if err = e.Write(buf); err != nil { 62 | panic(err) 63 | } 64 | // close the encoder to make sure the headers are properly 65 | // set and the data is flushed. 66 | if err = e.Close(); err != nil { 67 | panic(err) 68 | } 69 | out.Close() 70 | 71 | // reopen to confirm things worked well 72 | out, err = os.Open("testOutput/kick.wav") 73 | if err != nil { 74 | panic(err) 75 | } 76 | d2 := NewDecoder(out) 77 | d2.ReadInfo() 78 | fmt.Println("New file ->", d2) 79 | out.Close() 80 | os.Remove(out.Name()) 81 | 82 | // Output: 83 | // Old file -> Format: WAVE - 1 channels @ 22050 / 16 bits - Duration: 0.204172 seconds 84 | // New file -> Format: WAVE - 1 channels @ 22050 / 16 bits - Duration: 0.204172 seconds 85 | } 86 | 87 | func ExampleDecoder_ReadMetadata() { 88 | f, err := os.Open("fixtures/listinfo.wav") 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | defer f.Close() 93 | d := NewDecoder(f) 94 | d.ReadMetadata() 95 | if d.Err() != nil { 96 | log.Fatal(err) 97 | } 98 | fmt.Printf("%#v\n", d.Metadata) 99 | // Output: 100 | // &wav.Metadata{SamplerInfo:(*wav.SamplerInfo)(nil), Artist:"artist", Comments:"my comment", Copyright:"", CreationDate:"2017", Engineer:"", Technician:"", Genre:"genre", Keywords:"", Medium:"", Title:"track title", Product:"album title", Subject:"", Software:"", Source:"", Location:"", TrackNbr:"42", CuePoints:[]*wav.CuePoint(nil)} 101 | } 102 | -------------------------------------------------------------------------------- /fixtures/32bit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/32bit.wav -------------------------------------------------------------------------------- /fixtures/8bit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/8bit.wav -------------------------------------------------------------------------------- /fixtures/bass.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/bass.wav -------------------------------------------------------------------------------- /fixtures/bloop.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/bloop.aif -------------------------------------------------------------------------------- /fixtures/bwf.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/bwf.wav -------------------------------------------------------------------------------- /fixtures/dirty-kick-24b441k.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/dirty-kick-24b441k.wav -------------------------------------------------------------------------------- /fixtures/flloop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/flloop.wav -------------------------------------------------------------------------------- /fixtures/kick-16b441k.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/kick-16b441k.wav -------------------------------------------------------------------------------- /fixtures/kick.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/kick.wav -------------------------------------------------------------------------------- /fixtures/listChunkInHeader.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/listChunkInHeader.wav -------------------------------------------------------------------------------- /fixtures/listinfo.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/listinfo.wav -------------------------------------------------------------------------------- /fixtures/misaligned-chunk.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/misaligned-chunk.wav -------------------------------------------------------------------------------- /fixtures/padded24b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/padded24b.wav -------------------------------------------------------------------------------- /fixtures/sample.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-audio/wav/8b4315af26293f083333d7d02dea3cf12f4d1a56/fixtures/sample.avi -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-audio/wav 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-audio/aiff v1.0.0 7 | github.com/go-audio/audio v1.0.0 8 | github.com/go-audio/riff v1.0.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-audio/aiff v0.0.0-20180403003018-6c3a8a6aff12/go.mod h1:AMSAp6W1zd0koOdX6QDgGIuBDTUvLa2SLQtm7d9eM3c= 2 | github.com/go-audio/aiff v1.0.0 h1:Mn4zZvfGjTUc9VWXiTlj6BYUADTsmh3qZtpCyHFRMiE= 3 | github.com/go-audio/aiff v1.0.0/go.mod h1:Kazp+9JR/Y1ITCXaDlO6OIIOrz6eGGAn+dGT04V4HPM= 4 | github.com/go-audio/audio v0.0.0-20180206231410-b697a35b5608/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 5 | github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= 6 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 7 | github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= 8 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= 9 | github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks= 10 | github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s= 11 | -------------------------------------------------------------------------------- /list_chunk.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/go-audio/riff" 11 | ) 12 | 13 | var ( 14 | // See http://bwfmetaedit.sourceforge.net/listinfo.html 15 | markerIART = [4]byte{'I', 'A', 'R', 'T'} 16 | markerISFT = [4]byte{'I', 'S', 'F', 'T'} 17 | markerICRD = [4]byte{'I', 'C', 'R', 'D'} 18 | markerICOP = [4]byte{'I', 'C', 'O', 'P'} 19 | markerIARL = [4]byte{'I', 'A', 'R', 'L'} 20 | markerINAM = [4]byte{'I', 'N', 'A', 'M'} 21 | markerIENG = [4]byte{'I', 'E', 'N', 'G'} 22 | markerIGNR = [4]byte{'I', 'G', 'N', 'R'} 23 | markerIPRD = [4]byte{'I', 'P', 'R', 'D'} 24 | markerISRC = [4]byte{'I', 'S', 'R', 'C'} 25 | markerISBJ = [4]byte{'I', 'S', 'B', 'J'} 26 | markerICMT = [4]byte{'I', 'C', 'M', 'T'} 27 | markerITRK = [4]byte{'I', 'T', 'R', 'K'} 28 | markerITRKBug = [4]byte{'i', 't', 'r', 'k'} 29 | markerITCH = [4]byte{'I', 'T', 'C', 'H'} 30 | markerIKEY = [4]byte{'I', 'K', 'E', 'Y'} 31 | markerIMED = [4]byte{'I', 'M', 'E', 'D'} 32 | ) 33 | 34 | // DecodeListChunk decodes a LIST chunk 35 | func DecodeListChunk(d *Decoder, ch *riff.Chunk) error { 36 | if ch == nil { 37 | return fmt.Errorf("can't decode a nil chunk") 38 | } 39 | if d == nil { 40 | return fmt.Errorf("nil decoder") 41 | } 42 | if ch.ID == CIDList { 43 | // read the entire chunk in memory 44 | buf := make([]byte, ch.Size) 45 | var err error 46 | if _, err = ch.Read(buf); err != nil { 47 | return fmt.Errorf("failed to read the LIST chunk - %w", err) 48 | } 49 | r := bytes.NewReader(buf) 50 | // INFO subchunk 51 | scratch := make([]byte, 4) 52 | if _, err = r.Read(scratch); err != nil { 53 | return fmt.Errorf("failed to read the INFO subchunk - %w", err) 54 | } 55 | if !bytes.Equal(scratch, CIDInfo[:]) { 56 | // "expected an INFO subchunk but got %s", string(scratch) 57 | // TODO: support adtl subchunks 58 | ch.Drain() 59 | return nil 60 | } 61 | if d.Metadata == nil { 62 | d.Metadata = &Metadata{} 63 | } 64 | 65 | // the rest is a list of string entries 66 | var ( 67 | id [4]byte 68 | size uint32 69 | ) 70 | readSubHeader := func() error { 71 | if err := binary.Read(r, binary.BigEndian, &id); err != nil { 72 | return err 73 | } 74 | return binary.Read(r, binary.LittleEndian, &size) 75 | } 76 | 77 | // This checks and stops early if just a word alignment byte remains to avoid 78 | // an io.UnexpectedEOF error from readSubHeader. 79 | // TODO(steve): Remove the checks from the for statement if ch.Size is changed 80 | // to not include the padding byte. 81 | for rem := ch.Size - 4; rem > 1; rem -= int(size) + 8 { 82 | if err = readSubHeader(); err != nil { 83 | if errors.Is(err, io.EOF) { 84 | // All done. 85 | break 86 | } 87 | return fmt.Errorf("read sub header: %w", err) 88 | } 89 | 90 | if cap(scratch) >= int(size) { 91 | if len(scratch) != int(size) { 92 | // Resize scratch. 93 | scratch = scratch[:size] 94 | } 95 | } else { 96 | // Expand scratch capacity. 97 | scratch = append(make([]byte, int(size)-cap(scratch)), scratch[:cap(scratch)]...) 98 | } 99 | 100 | if _, err := r.Read(scratch); err != nil { 101 | return fmt.Errorf("read sub header %s data %v: %w", id, scratch, err) 102 | } 103 | 104 | switch id { 105 | case markerIARL: 106 | d.Metadata.Location = nullTermStr(scratch) 107 | case markerIART: 108 | d.Metadata.Artist = nullTermStr(scratch) 109 | case markerISFT: 110 | d.Metadata.Software = nullTermStr(scratch) 111 | case markerICRD: 112 | d.Metadata.CreationDate = nullTermStr(scratch) 113 | case markerICOP: 114 | d.Metadata.Copyright = nullTermStr(scratch) 115 | case markerINAM: 116 | d.Metadata.Title = nullTermStr(scratch) 117 | case markerIENG: 118 | d.Metadata.Engineer = nullTermStr(scratch) 119 | case markerIGNR: 120 | d.Metadata.Genre = nullTermStr(scratch) 121 | case markerIPRD: 122 | d.Metadata.Product = nullTermStr(scratch) 123 | case markerISRC: 124 | d.Metadata.Source = nullTermStr(scratch) 125 | case markerISBJ: 126 | d.Metadata.Subject = nullTermStr(scratch) 127 | case markerICMT: 128 | d.Metadata.Comments = nullTermStr(scratch) 129 | case markerITRK, markerITRKBug: 130 | d.Metadata.TrackNbr = nullTermStr(scratch) 131 | case markerITCH: 132 | d.Metadata.Technician = nullTermStr(scratch) 133 | case markerIKEY: 134 | d.Metadata.Keywords = nullTermStr(scratch) 135 | case markerIMED: 136 | d.Metadata.Medium = nullTermStr(scratch) 137 | } 138 | } 139 | } 140 | ch.Drain() 141 | return nil 142 | } 143 | 144 | func encodeInfoChunk(e *Encoder) []byte { 145 | if e == nil || e.Metadata == nil { 146 | return nil 147 | } 148 | buf := bytes.NewBuffer(nil) 149 | 150 | writeSection := func(id [4]byte, val string) { 151 | buf.Write(id[:]) 152 | binary.Write(buf, binary.LittleEndian, uint32(len(val)+1)) 153 | buf.Write(append([]byte(val), 0x00)) 154 | } 155 | if e.Metadata.Artist != "" { 156 | writeSection(markerIART, e.Metadata.Artist) 157 | } 158 | if e.Metadata.Comments != "" { 159 | writeSection(markerICMT, e.Metadata.Comments) 160 | } 161 | if e.Metadata.Copyright != "" { 162 | writeSection(markerICOP, e.Metadata.Copyright) 163 | } 164 | if e.Metadata.CreationDate != "" { 165 | writeSection(markerICRD, e.Metadata.CreationDate) 166 | } 167 | if e.Metadata.Engineer != "" { 168 | writeSection(markerIENG, e.Metadata.Engineer) 169 | } 170 | if e.Metadata.Technician != "" { 171 | writeSection(markerITCH, e.Metadata.Technician) 172 | } 173 | if e.Metadata.Genre != "" { 174 | writeSection(markerIGNR, e.Metadata.Genre) 175 | } 176 | if e.Metadata.Keywords != "" { 177 | writeSection(markerIKEY, e.Metadata.Keywords) 178 | } 179 | if e.Metadata.Medium != "" { 180 | writeSection(markerIMED, e.Metadata.Medium) 181 | } 182 | if e.Metadata.Title != "" { 183 | writeSection(markerINAM, e.Metadata.Title) 184 | } 185 | if e.Metadata.Product != "" { 186 | writeSection(markerIPRD, e.Metadata.Product) 187 | } 188 | if e.Metadata.Subject != "" { 189 | writeSection(markerISBJ, e.Metadata.Subject) 190 | } 191 | if e.Metadata.Software != "" { 192 | writeSection(markerISFT, e.Metadata.Software) 193 | } 194 | if e.Metadata.Source != "" { 195 | writeSection(markerISRC, e.Metadata.Source) 196 | } 197 | if e.Metadata.Location != "" { 198 | writeSection(markerIARL, e.Metadata.Location) 199 | } 200 | if e.Metadata.TrackNbr != "" { 201 | writeSection(markerITRK, e.Metadata.TrackNbr) 202 | } 203 | 204 | return append(CIDInfo, buf.Bytes()...) 205 | } 206 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | // Metadata represents optional metadata added to the wav file. 4 | type Metadata struct { 5 | SamplerInfo *SamplerInfo 6 | // Artist of the original subject of the file. For example, Michaelangelo. 7 | Artist string 8 | // Comments provides general comments about the file or the subject of the 9 | // file. If the comment is several sentences long, end each sentence with a 10 | // period. Do not include newline characters. 11 | Comments string 12 | // Copyright records the copyright information for the file. 13 | Copyright string 14 | // CreationDate specifies the date the subject of the file was created. List 15 | // dates in year-month-day format, padding one-digit months and days with a 16 | // zero on the left. For example: 1553-05-03 for May 3, 1553. The year 17 | // should always be given using four digits. 18 | CreationDate string 19 | // Engineer stores the name of the engineer who worked on the file. If there 20 | // are multiple engineers, separate the names by a semicolon and a blank. 21 | // For example: Smith, John; Adams, Joe. 22 | Engineer string 23 | // Technician identifies the technician who sampled the subject file. For 24 | // example: Smith, John. 25 | Technician string 26 | // Genre describes the original work, such as jazz, classical, rock, etc. 27 | Genre string 28 | // Keywords provides a list of keywords that refer to the file or subject of 29 | // the file. Separate multiple keywords with a semicolon and a blank. For 30 | // example, Seattle; zoology; The Civil War. 31 | Keywords string 32 | // Medium describes the original subject of the file, such as record, CD and so forth. 33 | Medium string 34 | // Title stores the title of the subject of the file, such as bohemian rhapsody. 35 | Title string 36 | // Product AKA album specifies the name of the title the file was originally 37 | // intended for: A Night at the Opera 38 | Product string 39 | // Subject describes the contents of the file, such as Metadata Management. 40 | Subject string 41 | // Software identifies the name of the software package used to create the 42 | // file, such as go-audio. 43 | Software string 44 | // Source identifies the name of the person or organization who supplied the 45 | // original subject of the file. For example: Splice. 46 | Source string 47 | // Location or Archival Location - Indicates where the subject of the file is archived. 48 | Location string 49 | // TrackNbr is the track number 50 | TrackNbr string 51 | // CuePoints is a list of cue points in the wav file. 52 | CuePoints []*CuePoint 53 | } 54 | 55 | // SamplerInfo is extra metadata pertinent to a sampler type usage. 56 | type SamplerInfo struct { 57 | // Manufacturer field specifies the MIDI Manufacturer's Association 58 | // (MMA) Manufacturer code for the sampler intended to receive this file's 59 | // waveform. Each manufacturer of a MIDI product is assigned a unique ID 60 | // which identifies the company. If no particular manufacturer is to be 61 | // specified, a value of 0 should be used. The value is stored with some 62 | // extra information to enable translation to the value used in a MIDI 63 | // System Exclusive transmission to the sampler. The high byte indicates the 64 | // number of low order bytes (1 or 3) that are valid for the manufacturer 65 | // code. For example, the value for Digidesign will be 0x01000013 (0x13) and 66 | // the value for Microsoft will be 0x30000041 (0x00, 0x00, 0x41). 67 | Manufacturer [4]byte 68 | // Product field specifies the MIDI model ID defined by the manufacturer 69 | // corresponding to the Manufacturer field. Contact the manufacturer of the 70 | // sampler to get the model ID. If no particular manufacturer's product is 71 | // to be specified, a value of 0 should be used. 72 | Product [4]byte 73 | // SamplePeriod The sample period specifies the duration of time that passes 74 | // during the playback of one sample in nanoseconds (normally equal to 1 / 75 | // Samplers Per Second, where Samples Per Second is the value found in the 76 | // format chunk). 77 | SamplePeriod uint32 78 | // MIDIUnityNote The MIDI unity note value has the same meaning as the instrument chunk's 79 | // MIDI Unshifted Note field which specifies the musical note at which the 80 | // sample will be played at it's original sample rate (the sample rate 81 | // specified in the format chunk). 82 | MIDIUnityNote uint32 83 | // MIDIPitchFraction The MIDI pitch fraction specifies the fraction of a 84 | // semitone up from the specified MIDI unity note field. A value of 85 | // 0x80000000 means 1/2 semitone (50 cents) and a value of 0x00000000 means 86 | // no fine tuning between semitones. 87 | MIDIPitchFraction uint32 88 | // SMPTEFormat The SMPTE format specifies the Society of Motion Pictures and 89 | // Television E time format used in the following SMPTE Offset field. If a 90 | // value of 0 is set, SMPTE Offset should also be set to 0. (0, 24, 25, 29, 30) 91 | SMPTEFormat uint32 92 | // SMPTEOffset The SMPTE Offset value specifies the time offset to be used 93 | // for the synchronization / calibration to the first sample in the 94 | // waveform. This value uses a format of 0xhhmmssff where hh is a signed 95 | // value that specifies the number of hours (-23 to 23), mm is an unsigned 96 | // value that specifies the number of minutes (0 to 59), ss is an unsigned 97 | // value that specifies the number of seconds (0 to 59) and ff is an 98 | // unsigned value that specifies the number of frames (0 to -1). 99 | SMPTEOffset uint32 100 | // NumSampleLoops The sample loops field specifies the number Sample Loop 101 | // definitions in the following list. This value may be set to 0 meaning 102 | // that no sample loops follow. 103 | NumSampleLoops uint32 104 | // Loops A list of sample loops is simply a set of consecutive loop 105 | // descriptions. The sample loops do not have to be in any particular order 106 | // because each sample loop associated cue point position is used to 107 | // determine the play order. 108 | Loops []*SampleLoop 109 | } 110 | 111 | // SampleLoop indicates a loop and its properties within the audio file 112 | type SampleLoop struct { 113 | // CuePointID - The Cue Point ID specifies the unique ID that corresponds to one of the 114 | // defined cue points in the cue point list. Furthermore, this ID 115 | // corresponds to any labels defined in the associated data list chunk which 116 | // allows text labels to be assigned to the various sample loops. 117 | CuePointID [4]byte 118 | // Type - The type field defines how the waveform samples will be looped. 119 | // 0 Loop forward (normal) 120 | // 1 Alternating loop (forward/backward, also known as Ping Pong) 121 | // 2 Loop backward (reverse) 122 | // 3 Reserved for future standard types 123 | // 32 - 0xFFFFFFFF Sampler specific types (defined by manufacturer) 124 | Type uint32 125 | // Start - The start value specifies the byte offset into the waveform data 126 | // of the first sample to be played in the loop. 127 | Start uint32 128 | // End - The end value specifies the byte offset into the waveform data of 129 | // the last sample to be played in the loop. 130 | End uint32 131 | // Fraction - The fractional value specifies a fraction of a sample at which 132 | // to loop. This allows a loop to be fine tuned at a resolution greater than 133 | // one sample. The value can range from 0x00000000 to 0xFFFFFFFF. A value of 134 | // 0 means no fraction, a value of 0x80000000 means 1/2 of a sample length. 135 | // 0xFFFFFFFF is the smallest fraction of a sample that can be represented. 136 | Fraction uint32 137 | // PlayCount - The play count value determines the number of times to play 138 | // the loop. A value of 0 specifies an infinite sustain loop. An infinite 139 | // sustain loop will continue looping until some external force interrupts 140 | // playback, such as the musician releasing the key that triggered the 141 | // wave's playback. All other values specify an absolute number of times to 142 | // loop. 143 | PlayCount uint32 144 | } 145 | -------------------------------------------------------------------------------- /metadata_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestDecoder_ReadMetadata(t *testing.T) { 11 | testCases := []struct { 12 | in string 13 | metadata *Metadata 14 | }{ 15 | {in: "fixtures/listinfo.wav", 16 | metadata: &Metadata{ 17 | Artist: "artist", Title: "track title", Product: "album title", 18 | TrackNbr: "42", CreationDate: "2017", Genre: "genre", Comments: "my comment", 19 | }, 20 | }, 21 | {in: "fixtures/kick.wav"}, 22 | {in: "fixtures/flloop.wav", metadata: &Metadata{ 23 | Software: "FL Studio (beta)", 24 | CuePoints: []*CuePoint{ 25 | 0: {ID: [4]uint8{0x1, 0x0, 0x0, 0x0}, Position: 0x0, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}}, 26 | 1: {ID: [4]uint8{0x2, 0x0, 0x0, 0x0}, Position: 0x1a5e, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x1a5e}, 27 | 2: {ID: [4]uint8{0x3, 0x0, 0x0, 0x0}, Position: 0x34bc, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x34bc}, 28 | 3: {ID: [4]uint8{0x4, 0x0, 0x0, 0x0}, Position: 0x4f1a, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x4f1a}, 29 | 4: {ID: [4]uint8{0x5, 0x0, 0x0, 0x0}, Position: 0x6978, DataChunkID: [4]uint8{'d', 'a', 't', 'a'}, SampleOffset: 0x6978}, 30 | 5: {ID: [4]uint8{0x6, 0x0, 0x0, 0x0}, Position: 0x83d6, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x83d6}, 31 | 6: {ID: [4]uint8{0x7, 0x0, 0x0, 0x0}, Position: 0x9e34, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x9e34}, 32 | 7: {ID: [4]uint8{0x8, 0x0, 0x0, 0x0}, Position: 0xb892, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xb892}, 33 | 8: {ID: [4]uint8{0x9, 0x0, 0x0, 0x0}, Position: 0xd2f0, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xd2f0}, 34 | 9: {ID: [4]uint8{0xa, 0x0, 0x0, 0x0}, Position: 0xed4e, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0xed4e}, 35 | 10: {ID: [4]uint8{0xb, 0x0, 0x0, 0x0}, Position: 0x107ac, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x107ac}, 36 | 11: {ID: [4]uint8{0xc, 0x0, 0x0, 0x0}, Position: 0x1220a, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x1220a}, 37 | 12: {ID: [4]uint8{0xd, 0x0, 0x0, 0x0}, Position: 0x13c68, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x13c68}, 38 | 13: {ID: [4]uint8{0xe, 0x0, 0x0, 0x0}, Position: 0x156c6, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x156c6}, 39 | 14: {ID: [4]uint8{0xf, 0x0, 0x0, 0x0}, Position: 0x17124, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x17124}, 40 | 15: {ID: [4]uint8{0x10, 0x0, 0x0, 0x0}, Position: 0x18b82, DataChunkID: [4]uint8{0x64, 0x61, 0x74, 0x61}, SampleOffset: 0x18b82}, 41 | }, 42 | SamplerInfo: &SamplerInfo{SamplePeriod: 22676, MIDIUnityNote: 60, NumSampleLoops: 1, 43 | Loops: []*SampleLoop{ 44 | {CuePointID: [4]byte{0, 0, 2, 0}, Type: 1024, Start: 0, End: 107999, Fraction: 0, PlayCount: 0}, 45 | }}, 46 | }}, 47 | } 48 | 49 | for _, tc := range testCases { 50 | t.Run(path.Base(tc.in), func(t *testing.T) { 51 | f, err := os.Open(tc.in) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | d := NewDecoder(f) 56 | d.ReadMetadata() 57 | if err = d.Err(); err != nil { 58 | t.Fatal(err) 59 | } 60 | if tc.metadata != nil { 61 | if tc.metadata.SamplerInfo != nil { 62 | if !reflect.DeepEqual(tc.metadata.SamplerInfo, d.Metadata.SamplerInfo) { 63 | t.Fatalf("Expected sampler info\n%#v to equal\n%#v\n", d.Metadata.SamplerInfo, tc.metadata.SamplerInfo) 64 | } 65 | } 66 | if tc.metadata.CuePoints != nil { 67 | if !reflect.DeepEqual(tc.metadata.CuePoints, d.Metadata.CuePoints) { 68 | for i, c := range d.Metadata.CuePoints { 69 | if !reflect.DeepEqual(c, tc.metadata.CuePoints[i]) { 70 | t.Errorf("[%d] expected %#v got %#v", i, tc.metadata.CuePoints[i], c) 71 | } 72 | } 73 | t.Errorf("Expected cue points\n%#v to equal\n%#v\n", d.Metadata.CuePoints, tc.metadata.CuePoints) 74 | } 75 | } 76 | 77 | if !reflect.DeepEqual(tc.metadata, d.Metadata) { 78 | t.Fatalf("Expected\n%#v\n to equal\n%#v\n", d.Metadata, tc.metadata) 79 | } 80 | } 81 | f.Close() 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /smpl_chunk.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | "github.com/go-audio/riff" 9 | ) 10 | 11 | // smpl chunk is documented here: 12 | // https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#smpl 13 | 14 | // DecodeSamplerChunk decodes a smpl chunk and put the data in Decoder.Metadata.SamplerInfo 15 | func DecodeSamplerChunk(d *Decoder, ch *riff.Chunk) error { 16 | if ch == nil { 17 | return fmt.Errorf("can't decode a nil chunk") 18 | } 19 | if d == nil { 20 | return fmt.Errorf("nil decoder") 21 | } 22 | if ch.ID == CIDSmpl { 23 | // read the entire chunk in memory 24 | buf := make([]byte, ch.Size) 25 | var err error 26 | if _, err = ch.Read(buf); err != nil { 27 | return fmt.Errorf("failed to read the smpl chunk - %w", err) 28 | } 29 | if d.Metadata == nil { 30 | d.Metadata = &Metadata{} 31 | } 32 | 33 | d.Metadata.SamplerInfo = &SamplerInfo{} 34 | 35 | r := bytes.NewReader(buf) 36 | 37 | scratch := make([]byte, 4) 38 | if _, err = r.Read(scratch); err != nil { 39 | return fmt.Errorf("failed to read the smpl Manufacturer") 40 | } 41 | copy(d.Metadata.SamplerInfo.Manufacturer[:], scratch[:4]) 42 | if _, err = r.Read(scratch); err != nil { 43 | return fmt.Errorf("failed to read the smpl Product") 44 | } 45 | copy(d.Metadata.SamplerInfo.Product[:], scratch[:4]) 46 | 47 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.SamplePeriod); err != nil { 48 | return err 49 | } 50 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.MIDIUnityNote); err != nil { 51 | return err 52 | } 53 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.MIDIPitchFraction); err != nil { 54 | return err 55 | } 56 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.SMPTEFormat); err != nil { 57 | return err 58 | } 59 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.SMPTEOffset); err != nil { 60 | return err 61 | } 62 | if err := binary.Read(r, binary.LittleEndian, &d.Metadata.SamplerInfo.NumSampleLoops); err != nil { 63 | return err 64 | } 65 | var remaining uint32 66 | // sampler data 67 | if err := binary.Read(r, binary.BigEndian, &remaining); err != nil { 68 | return err 69 | } 70 | if d.Metadata.SamplerInfo.NumSampleLoops > 0 { 71 | d.Metadata.SamplerInfo.Loops = []*SampleLoop{} 72 | for i := uint32(0); i < d.Metadata.SamplerInfo.NumSampleLoops; i++ { 73 | sl := &SampleLoop{} 74 | if _, err = r.Read(scratch); err != nil { 75 | return fmt.Errorf("failed to read the sample loop cue point id") 76 | } 77 | copy(sl.CuePointID[:], scratch[:4]) 78 | if err := binary.Read(r, binary.LittleEndian, &sl.Type); err != nil { 79 | return err 80 | } 81 | if err := binary.Read(r, binary.LittleEndian, &sl.Start); err != nil { 82 | return err 83 | } 84 | if err := binary.Read(r, binary.LittleEndian, &sl.End); err != nil { 85 | return err 86 | } 87 | if err := binary.Read(r, binary.LittleEndian, &sl.Fraction); err != nil { 88 | return err 89 | } 90 | if err := binary.Read(r, binary.LittleEndian, &sl.PlayCount); err != nil { 91 | return err 92 | } 93 | 94 | d.Metadata.SamplerInfo.Loops = append(d.Metadata.SamplerInfo.Loops, sl) 95 | } 96 | } 97 | } 98 | ch.Drain() 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /wav.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package wav is a package allowing developers to decode and encode audio PCM 3 | data using the Waveform Audio File Format https://en.wikipedia.org/wiki/WAV 4 | */ 5 | package wav 6 | 7 | import ( 8 | "errors" 9 | "math" 10 | "time" 11 | ) 12 | 13 | var ( 14 | // ErrPCMChunkNotFound indicates a bad audio file without data 15 | ErrPCMChunkNotFound = errors.New("PCM Chunk not found in audio file") 16 | ) 17 | 18 | func nullTermStr(b []byte) string { 19 | return string(b[:clen(b)]) 20 | } 21 | 22 | func clen(n []byte) int { 23 | for i := 0; i < len(n); i++ { 24 | if n[i] == 0 { 25 | return i 26 | } 27 | } 28 | return len(n) 29 | } 30 | 31 | func bytesNumFromDuration(dur time.Duration, sampleRate, bitDepth int) int { 32 | k := bitDepth / 8 33 | return samplesNumFromDuration(dur, sampleRate) * k 34 | } 35 | 36 | func samplesNumFromDuration(dur time.Duration, sampleRate int) int { 37 | return int(math.Floor(float64(dur / sampleDuration(sampleRate)))) 38 | } 39 | 40 | func sampleDuration(sampleRate int) time.Duration { 41 | if sampleRate == 0 { 42 | return 0 43 | } 44 | return time.Second / time.Duration(math.Abs(float64(sampleRate))) 45 | } 46 | --------------------------------------------------------------------------------