├── .github └── workflows │ └── tests.yml ├── LICENSE ├── Readme.md ├── cmd └── resampler │ └── main.go ├── go.mod ├── resample.go ├── resample_test.go └── testing ├── piano-16k-16-1.wav ├── piano-16k-16-2.wav ├── piano-44.1k-16-2.wav ├── piano-44.1k-32f-2.wav └── piano-48k-16-2.wav /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: '>=1.20.0' 18 | - name: Install packages 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -y pkg-config libsoxr0 libsoxr-dev 22 | - name: Run tests 23 | run: go test -v 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2024, Eleftherios (Lefteris) Zafiris 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of 8 | conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 11 | conditions and the following disclaimer in the documentation and/or other materials 12 | provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 20 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 22 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 23 | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 25 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # resample 2 | -- 3 | import "github.com/zaf/resample" 4 | 5 | Package resample implements resampling of PCM-encoded audio. It uses the SoX 6 | Resampler library `libsoxr'. 7 | 8 | To install make sure you have libsoxr and pkg-config installed, then run: 9 | 10 | go install github.com/zaf/resample@latest 11 | 12 | The package warps an io.Reader in a Resampler that resamples and writes all 13 | input data. Input should be RAW PCM encoded audio samples. 14 | 15 | For usage details please see the code snippet in the cmd folder. 16 | 17 | ## Usage 18 | 19 | ```go 20 | const ( 21 | // Quality settings 22 | Quick = 0 // Quick cubic interpolation 23 | LowQ = 1 // LowQ 16-bit with larger rolloff 24 | MediumQ = 2 // MediumQ 16-bit with medium rolloff 25 | HighQ = 4 // High quality 26 | VeryHighQ = 6 // Very high quality 27 | 28 | // Input formats 29 | F32 = 0 // 32-bit floating point PCM 30 | F64 = 1 // 64-bit floating point PCM 31 | I32 = 2 // 32-bit signed linear PCM 32 | I16 = 3 // 16-bit signed linear PCM 33 | 34 | ) 35 | ``` 36 | 37 | #### type Resampler 38 | 39 | ```go 40 | type Resampler struct { 41 | } 42 | ``` 43 | 44 | Resampler resamples PCM sound data. 45 | 46 | #### func New 47 | 48 | ```go 49 | func New(writer io.Writer, inputRate, outputRate float64, channels, format, quality int) (*Resampler, error) 50 | ``` 51 | New returns a pointer to a Resampler that implements an io.WriteCloser. It takes 52 | as parameters the destination data Writer, the input and output sampling rates, 53 | the number of channels of the input data, the input format and the quality setting. 54 | 55 | #### func (*Resampler) Close 56 | 57 | ```go 58 | func (r *Resampler) Close() (error) 59 | ``` 60 | Close flushes, clean-ups and frees memory. Should always be called when finished using 61 | the resampler, and before we can use its output. 62 | 63 | #### func (*Resampler) Reset 64 | 65 | ```go 66 | func (r *Resampler) Reset(writer io.Writer) (error) 67 | ``` 68 | Reset permits reusing a Resampler rather than allocating a new one. 69 | 70 | #### func (*Resampler) Write 71 | 72 | ```go 73 | func (r *Resampler) Write(p []byte) (int, error) 74 | ``` 75 | Write resamples PCM sound data. Writes len(p) bytes from p to the underlying 76 | data stream, returns the number of bytes written from p (0 <= n <= len(p)) and 77 | any error encountered that caused the write to stop early. 78 | -------------------------------------------------------------------------------- /cmd/resampler/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 - 2024, Lefteris Zafiris 3 | 4 | This program is free software, distributed under the terms of 5 | the BSD 3-Clause License. See the LICENSE file 6 | at the top of the source tree. 7 | */ 8 | 9 | // The program takes as input a WAV or RAW PCM sound file 10 | // and resamples it to the desired sampling rate. 11 | // The output is RAW PCM data. 12 | // Usage: goresample [flags] input_file output_file 13 | // 14 | // Example: go run main.go -ir 16000 -or 8000 ../../testing/piano-16k-16-2.wav 8k.raw 15 | 16 | package main 17 | 18 | import ( 19 | "flag" 20 | "io" 21 | "log" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | "github.com/zaf/resample" 27 | ) 28 | 29 | const wavHeader = 44 30 | 31 | var ( 32 | format = flag.String("format", "i16", "PCM format") 33 | ch = flag.Int("ch", 2, "Number of channels") 34 | ir = flag.Int("ir", 44100, "Input sample rate") 35 | or = flag.Int("or", 0, "Output sample rate") 36 | ) 37 | 38 | func main() { 39 | flag.Parse() 40 | var frmt int 41 | switch *format { 42 | case "i16": 43 | frmt = resample.I16 44 | case "i32": 45 | frmt = resample.I32 46 | case "f32": 47 | frmt = resample.F32 48 | case "f64": 49 | frmt = resample.F64 50 | default: 51 | log.Fatalln("Invalid Format") 52 | } 53 | if *ch < 1 { 54 | log.Fatalln("Invalid channel number") 55 | } 56 | if *ir <= 0 || *or <= 0 { 57 | log.Fatalln("Invalid input or output sample rate") 58 | } 59 | if flag.NArg() < 2 { 60 | log.Fatalln("No input or output files given") 61 | } 62 | inputFile := flag.Arg(0) 63 | outputFile := flag.Arg(1) 64 | var err error 65 | 66 | // Open input file (WAV or RAW PCM) 67 | input, err := os.Open(inputFile) 68 | if err != nil { 69 | log.Fatalln(err) 70 | } 71 | defer input.Close() 72 | output, err := os.Create(outputFile) 73 | if err != nil { 74 | log.Fatalln(err) 75 | } 76 | // Create a Resampler 77 | res, err := resample.New(output, float64(*ir), float64(*or), *ch, frmt, resample.HighQ) 78 | if err != nil { 79 | output.Close() 80 | os.Remove(outputFile) 81 | log.Fatalln(err) 82 | } 83 | // Skip WAV file header in order to pass only the PCM data to the Resampler 84 | if strings.ToLower(filepath.Ext(inputFile)) == ".wav" { 85 | input.Seek(wavHeader, 0) 86 | } 87 | 88 | // Read input and pass it to the Resampler in chunks 89 | _, err = io.Copy(res, input) 90 | // Close the Resampler and the output file. Clsoing the Resampler will flush any remaining data to the output file. 91 | // If the Resampler is not closed before the output file, any remaining data will be lost. 92 | res.Close() 93 | output.Close() 94 | if err != nil { 95 | os.Remove(outputFile) 96 | log.Fatalln(err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zaf/resample 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /resample.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 - 2024, Lefteris Zafiris 3 | 4 | This program is free software, distributed under the terms of 5 | the BSD 3-Clause License. See the LICENSE file 6 | at the top of the source tree. 7 | */ 8 | 9 | /* 10 | Package resample implements resampling of PCM-encoded audio. 11 | It uses the SoX Resampler library `libsoxr'. 12 | 13 | To install make sure you have libsoxr and pkg-config installed, then run: 14 | 15 | go install github.com/zaf/resample@latest 16 | 17 | The package warps an io.Reader in a Resampler that resamples and 18 | writes all input data. Input should be RAW PCM encoded audio samples. 19 | 20 | For usage details please see the code snippet in the cmd folder. 21 | */ 22 | package resample 23 | 24 | /* 25 | // Link soxr using pkg-config. 26 | #cgo pkg-config: soxr 27 | #include 28 | #include 29 | */ 30 | import "C" 31 | import ( 32 | "errors" 33 | "io" 34 | "runtime" 35 | "unsafe" 36 | ) 37 | 38 | const ( 39 | // Quality settings 40 | Quick = 0 // Quick cubic interpolation 41 | LowQ = 1 // LowQ 16-bit with larger rolloff 42 | MediumQ = 2 // MediumQ 16-bit with medium rolloff 43 | HighQ = 4 // High quality 44 | VeryHighQ = 6 // Very high quality 45 | 46 | // Input formats 47 | F32 = 0 // 32-bit floating point PCM 48 | F64 = 1 // 64-bit floating point PCM 49 | I32 = 2 // 32-bit signed linear PCM 50 | I16 = 3 // 16-bit signed linear PCM 51 | 52 | byteLen = 8 53 | ) 54 | 55 | // Resampler resamples PCM sound data. 56 | type Resampler struct { 57 | resampler C.soxr_t 58 | inRate float64 // input sample rate 59 | outRate float64 // output sample rate 60 | channels int // number of input channels 61 | frameSize int // frame size in bytes 62 | destination io.Writer // output data 63 | } 64 | 65 | var threads int 66 | 67 | func init() { 68 | threads = runtime.NumCPU() 69 | } 70 | 71 | // New returns a pointer to a Resampler that implements an io.WriteCloser. 72 | // It takes as parameters the destination data Writer, the input and output 73 | // sampling rates, the number of channels of the input data, the input format 74 | // and the quality setting. 75 | func New(writer io.Writer, inputRate, outputRate float64, channels, format, quality int) (*Resampler, error) { 76 | var err error 77 | var size int 78 | if writer == nil { 79 | return nil, errors.New("io.Writer is nil") 80 | } 81 | if inputRate <= 0 || outputRate <= 0 { 82 | return nil, errors.New("invalid input or output sampling rates") 83 | } 84 | if channels == 0 { 85 | return nil, errors.New("invalid channels number") 86 | } 87 | if quality < 0 || quality > 6 { 88 | return nil, errors.New("invalid quality setting") 89 | } 90 | switch format { 91 | case F64: 92 | size = 64 / byteLen 93 | case F32, I32: 94 | size = 32 / byteLen 95 | case I16: 96 | size = 16 / byteLen 97 | default: 98 | return nil, errors.New("invalid format setting") 99 | } 100 | var soxr C.soxr_t 101 | var soxErr C.soxr_error_t 102 | // Setup soxr and create a stream resampler 103 | ioSpec := C.soxr_io_spec(C.soxr_datatype_t(format), C.soxr_datatype_t(format)) 104 | qSpec := C.soxr_quality_spec(C.ulong(quality), 0) 105 | runtimeSpec := C.soxr_runtime_spec(C.uint(threads)) 106 | soxr = C.soxr_create(C.double(inputRate), C.double(outputRate), C.uint(channels), &soxErr, &ioSpec, &qSpec, &runtimeSpec) 107 | if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" { 108 | err = errors.New(C.GoString(soxErr)) 109 | C.free(unsafe.Pointer(soxErr)) 110 | return nil, err 111 | } 112 | 113 | r := Resampler{ 114 | resampler: soxr, 115 | inRate: inputRate, 116 | outRate: outputRate, 117 | channels: channels, 118 | frameSize: size, 119 | destination: writer, 120 | } 121 | C.free(unsafe.Pointer(soxErr)) 122 | return &r, err 123 | } 124 | 125 | // Reset permits reusing a Resampler rather than allocating a new one. 126 | func (r *Resampler) Reset(writer io.Writer) error { 127 | var err error 128 | if r.resampler == nil { 129 | return errors.New("soxr resampler is nil") 130 | } 131 | err = r.flush() 132 | r.destination = writer 133 | C.soxr_clear(r.resampler) 134 | return err 135 | } 136 | 137 | // Close flushes, clean-ups and frees memory. Should always be called when 138 | // finished using the resampler. Should always be called when finished using 139 | // the resampler, and before we can use its output. 140 | func (r *Resampler) Close() error { 141 | var err error 142 | if r.resampler == nil { 143 | return errors.New("soxr resampler is nil") 144 | } 145 | err = r.flush() 146 | C.soxr_delete(r.resampler) 147 | r.resampler = nil 148 | return err 149 | } 150 | 151 | // Write resamples PCM sound data. Writes len(p) bytes from p to 152 | // the underlying data stream, returns the number of bytes written 153 | // from p (0 <= n <= len(p)) and any error encountered that caused 154 | // the write to stop early. 155 | func (r *Resampler) Write(p []byte) (int, error) { 156 | var err error 157 | var i int 158 | if r.resampler == nil { 159 | return i, errors.New("soxr resampler is nil") 160 | } 161 | if len(p) == 0 { 162 | return i, nil 163 | } 164 | framesIn := len(p) / r.frameSize / r.channels 165 | if framesIn == 0 { 166 | return i, errors.New("incomplete input frame data") 167 | } 168 | framesOut := int(float64(framesIn) * (r.outRate / r.inRate)) 169 | if framesOut == 0 { 170 | return i, errors.New("not enough input to generate output") 171 | } 172 | dataIn := C.CBytes(p) 173 | dataOut := C.malloc(C.size_t(framesOut * r.channels * r.frameSize)) 174 | var soxErr C.soxr_error_t 175 | var read, done C.size_t = 0, 0 176 | soxErr = C.soxr_process(r.resampler, C.soxr_in_t(dataIn), C.size_t(framesIn), &read, C.soxr_out_t(dataOut), C.size_t(framesOut), &done) 177 | if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" { 178 | err = errors.New(C.GoString(soxErr)) 179 | goto cleanup 180 | } 181 | _, err = r.destination.Write(C.GoBytes(dataOut, C.int(int(done)*r.channels*r.frameSize))) 182 | // In many cases the resampler will not return the full data unless we flush it. Espasially if the input chunck is small 183 | // As long as we close the resampler (Close() flushes all data) we don't need to worry about short writes, unless r.destination.Write() fails 184 | if err == nil { 185 | i = len(p) 186 | } 187 | cleanup: 188 | C.free(dataIn) 189 | C.free(dataOut) 190 | C.free(unsafe.Pointer(soxErr)) 191 | return i, err 192 | } 193 | 194 | // flush any pending output from the resampler. Aftter that no more input can be passed. 195 | func (r *Resampler) flush() error { 196 | var err error 197 | var done C.size_t 198 | var soxErr C.soxr_error_t 199 | framesOut := 4096 * 16 200 | dataOut := C.malloc(C.size_t(framesOut * r.channels * r.frameSize)) 201 | // Flush any pending output by calling soxr_process with no input data. 202 | soxErr = C.soxr_process(r.resampler, nil, 0, nil, C.soxr_out_t(dataOut), C.size_t(framesOut), &done) 203 | if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" { 204 | err = errors.New(C.GoString(soxErr)) 205 | goto cleanup 206 | } 207 | _, err = r.destination.Write(C.GoBytes(dataOut, C.int(int(done)*r.channels*r.frameSize))) 208 | cleanup: 209 | C.free(dataOut) 210 | C.free(unsafe.Pointer(soxErr)) 211 | return err 212 | } 213 | -------------------------------------------------------------------------------- /resample_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 - 2024, Lefteris Zafiris 3 | 4 | This program is free software, distributed under the terms of 5 | the BSD 3-Clause License. See the LICENSE file 6 | at the top of the source tree. 7 | */ 8 | 9 | package resample 10 | 11 | import ( 12 | "bytes" 13 | "io" 14 | "os" 15 | "testing" 16 | ) 17 | 18 | var NewTest = []struct { 19 | writer io.Writer 20 | inputRate float64 21 | outputRate float64 22 | channels int 23 | format int 24 | quality int 25 | err string 26 | }{ 27 | {writer: io.Discard, inputRate: 8000.0, outputRate: 8000.0, channels: 1, format: I16, quality: MediumQ, err: ""}, 28 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: MediumQ, err: ""}, 29 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I32, quality: MediumQ, err: ""}, 30 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: F32, quality: MediumQ, err: ""}, 31 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: F64, quality: MediumQ, err: ""}, 32 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: Quick, err: ""}, 33 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: LowQ, err: ""}, 34 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: HighQ, err: ""}, 35 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: VeryHighQ, err: ""}, 36 | {writer: nil, inputRate: 8000.0, outputRate: 8000.0, channels: 2, format: I16, quality: MediumQ, err: "io.Writer is nil"}, 37 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 0, format: I16, quality: MediumQ, err: "invalid channels number"}, 38 | {writer: io.Discard, inputRate: 16000.0, outputRate: 0.0, channels: 0, format: I16, quality: MediumQ, err: "invalid input or output sampling rates"}, 39 | {writer: io.Discard, inputRate: 0.0, outputRate: 8000.0, channels: 0, format: I16, quality: MediumQ, err: "invalid input or output sampling rates"}, 40 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: 10, quality: MediumQ, err: "invalid format setting"}, 41 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: 10, err: "invalid quality setting"}, 42 | {writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: -10, err: "invalid quality setting"}, 43 | } 44 | 45 | func TestNew(t *testing.T) { 46 | for _, tc := range NewTest { 47 | res, err := New(tc.writer, tc.inputRate, tc.outputRate, tc.channels, tc.format, tc.quality) 48 | if err != nil && tc.err != err.Error() { 49 | t.Fatalf("Expecting: %s got: %v", tc.err, err) 50 | } 51 | if err == nil && tc.err != "" { 52 | t.Fatalf("No error for: %s", tc.err) 53 | } 54 | if res != nil { 55 | res.Close() 56 | } 57 | } 58 | } 59 | 60 | var WriteTest = []struct { 61 | name string 62 | inputRate float64 63 | outputRate float64 64 | channels int 65 | testData []struct { 66 | data []byte 67 | expected int 68 | err string 69 | } 70 | }{ 71 | {"1-1 Resampler mono", 8000.0, 8000.0, 1, []struct { 72 | data []byte 73 | expected int 74 | err string 75 | }{ 76 | {[]byte{}, 0, ""}, 77 | {[]byte{0x01}, 0, "incomplete input frame data"}, 78 | {[]byte{0x01, 0x00}, 2, ""}, 79 | {[]byte{0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde}, 24, ""}, 80 | {[]byte{0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0xd9}, 25, ""}, 81 | }}, 82 | 83 | {"1-2 Resampler stereo", 8000.0, 16000.0, 2, []struct { 84 | data []byte 85 | expected int 86 | err string 87 | }{ 88 | {[]byte{}, 0, ""}, 89 | {[]byte{0x01}, 0, "incomplete input frame data"}, 90 | }}, 91 | 92 | {"2-1 Resampler mono", 8000.0, 4000.0, 2, []struct { 93 | data []byte 94 | expected int 95 | err string 96 | }{ 97 | {[]byte{}, 0, ""}, 98 | {[]byte{0x01}, 0, "incomplete input frame data"}, 99 | {[]byte{0x01, 0x00, 0x7c, 0x7f}, 0, "not enough input to generate output"}, 100 | }}, 101 | } 102 | 103 | func TestWrite(t *testing.T) { 104 | for _, tc := range WriteTest { 105 | res, err := New(io.Discard, tc.inputRate, tc.outputRate, tc.channels, I16, MediumQ) 106 | if err != nil { 107 | t.Fatal("Failed to create a", tc.name, "Resampler:", err) 108 | } 109 | for _, td := range tc.testData { 110 | i, err := res.Write(td.data) 111 | res.Reset(io.Discard) 112 | if err != nil && err.Error() != td.err { 113 | t.Errorf("Resampler %s writer, error: %s, expecting: %s", tc.name, err.Error(), td.err) 114 | } 115 | if err == nil && td.err != "" { 116 | t.Errorf("Resampler %s writer, expecting: %s", tc.name, td.err) 117 | } 118 | if err != nil { 119 | continue 120 | } 121 | if i != td.expected { 122 | t.Errorf("Resampler %s writer, returned: %d, expecting: %d", tc.name, i, td.expected) 123 | } 124 | } 125 | } 126 | } 127 | 128 | var FileTest = []struct { 129 | file string 130 | inputRate float64 131 | outputRate float64 132 | channels int 133 | format int 134 | quality int 135 | }{ 136 | {"testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, MediumQ}, 137 | {"testing/piano-16k-16-2.wav", 16000.0, 4000.0, 2, I16, MediumQ}, 138 | //{"testing/piano-44.1k-16-2.wav", 44100.0, 22050.0, 2, I16, MediumQ}, 139 | //{"testing/piano-44.1k-32f-2.wav", 44100.0, 48000.0, 2, F32, MediumQ}, 140 | //{"testing/piano-48k-16-2.wav", 48000, 44100.0, 2, I16, MediumQ}, 141 | } 142 | 143 | func TestFile(t *testing.T) { 144 | for _, td := range FileTest { 145 | input, err := os.ReadFile(td.file) 146 | if err != nil { 147 | t.Fatal("Failed to read test data:", err) 148 | } 149 | var out bytes.Buffer 150 | res, err := New(&out, td.inputRate, td.outputRate, td.channels, td.format, td.quality) 151 | if err != nil { 152 | t.Fatal("Failed to create a Resampler:", err) 153 | } 154 | _, err = res.Write(input[44:]) 155 | if err != nil { 156 | t.Errorf("Write failed: %s", err) 157 | } 158 | err = res.Close() 159 | if err != nil { 160 | t.Fatal("Failed to close Resampler:", err) 161 | } 162 | inSize := float64(len(input[44:])) 163 | outSize := float64(out.Len()) * td.inputRate / td.outputRate 164 | if inSize != outSize { 165 | t.Error("Resampled file size mismatch, in:", int(inSize), "out:", int(outSize)) 166 | } 167 | } 168 | } 169 | 170 | func TestClose(t *testing.T) { 171 | res, err := New(io.Discard, 16000.0, 8000.0, 1, I16, MediumQ) 172 | if err != nil { 173 | t.Fatal("Failed to create a Resampler:", err) 174 | } 175 | err = res.Close() 176 | if err != nil { 177 | t.Fatal("Failed to Close the Resampler:", err) 178 | } 179 | _, err = res.Write(WriteTest[0].testData[3].data) 180 | if err == nil { 181 | t.Fatal("Running Write on a closed Resampler didn't return an error.") 182 | } 183 | err = res.Close() 184 | if err == nil { 185 | t.Fatal("Running Close on a closed Resampler didn't return an error.") 186 | } 187 | } 188 | 189 | func TestReset(t *testing.T) { 190 | res, err := New(io.Discard, 16000.0, 8000.0, 1, I16, MediumQ) 191 | if err != nil { 192 | t.Fatal("Failed to create a Resampler:", err) 193 | } 194 | err = res.Reset(io.Discard) 195 | if err != nil { 196 | t.Fatal("Failed to Reset the Resampler:", err) 197 | } 198 | err = res.Close() 199 | if err != nil { 200 | t.Fatal("Failed to Close the Resampler:", err) 201 | } 202 | err = res.Reset(io.Discard) 203 | if err == nil { 204 | t.Fatal("Running Reset on a closed Resampler didn't return an error.") 205 | } 206 | } 207 | 208 | // Benchmarking data 209 | var BenchData = []struct { 210 | name string 211 | file string 212 | inRate float64 213 | outRate float64 214 | channels int 215 | format int 216 | quality int 217 | }{ 218 | {"16bit 2 ch 44,1->16 Medium", "testing/piano-44.1k-16-2.wav", 44100.0, 16000.0, 2, I16, MediumQ}, 219 | {"16bit 2 ch 16->8 Medium", "testing/piano-16k-16-2.wav", 16000.0, 8000.0, 2, I16, MediumQ}, 220 | {"32fl 2 ch 44.1->8 Medium", "testing/piano-44.1k-32f-2.wav", 44100.0, 8000.0, 2, F32, MediumQ}, 221 | {"16bit 2 ch 44.1->48 Medium", "testing/piano-44.1k-16-2.wav", 44100.0, 48000.0, 2, I16, MediumQ}, 222 | {"16bit 2 ch 48->44.1 Medium", "testing/piano-48k-16-2.wav", 48000.0, 44100.0, 2, I16, MediumQ}, 223 | {"16bit 1 ch 16->8 Quick", "testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, Quick}, 224 | {"16bit 1 ch 16->8 Low", "testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, LowQ}, 225 | {"16bit 1 ch 16->8 Medium", "testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, MediumQ}, 226 | {"16bit 1 ch 16->8 High", "testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, HighQ}, 227 | {"16bit 1 ch 16->8 VeryHigh", "testing/piano-16k-16-1.wav", 16000.0, 8000.0, 1, I16, VeryHighQ}, 228 | } 229 | 230 | func BenchmarkResampling(b *testing.B) { 231 | for _, bd := range BenchData { 232 | b.Run(bd.name, func(b *testing.B) { 233 | rawData, err := os.ReadFile(bd.file) 234 | if err != nil { 235 | b.Fatalf("Failed to read test data: %s\n", err) 236 | } 237 | b.SetBytes(int64(len(rawData[44:]))) 238 | res, err := New(io.Discard, bd.inRate, bd.outRate, bd.channels, bd.format, bd.quality) 239 | if err != nil { 240 | b.Fatalf("Failed to create Writer: %s\n", err) 241 | } 242 | b.ResetTimer() 243 | for i := 0; i < b.N; i++ { 244 | _, err = res.Write(rawData[44:]) 245 | if err != nil { 246 | b.Fatalf("Encoding failed: %s\n", err) 247 | } 248 | } 249 | res.Close() 250 | }) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /testing/piano-16k-16-1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaf/resample/4180beb222ee862d26db681bfad5d95342b134be/testing/piano-16k-16-1.wav -------------------------------------------------------------------------------- /testing/piano-16k-16-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaf/resample/4180beb222ee862d26db681bfad5d95342b134be/testing/piano-16k-16-2.wav -------------------------------------------------------------------------------- /testing/piano-44.1k-16-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaf/resample/4180beb222ee862d26db681bfad5d95342b134be/testing/piano-44.1k-16-2.wav -------------------------------------------------------------------------------- /testing/piano-44.1k-32f-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaf/resample/4180beb222ee862d26db681bfad5d95342b134be/testing/piano-44.1k-32f-2.wav -------------------------------------------------------------------------------- /testing/piano-48k-16-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaf/resample/4180beb222ee862d26db681bfad5d95342b134be/testing/piano-48k-16-2.wav --------------------------------------------------------------------------------