├── .github └── workflows │ └── tests.yml ├── LICENSE ├── README.md ├── convolver.go ├── convolver_test.go ├── fft.go ├── fft_test.go ├── filter ├── filter.go └── filter_test.go ├── go.mod ├── go.sum ├── twiddle.go └── window ├── window.go └── window_test.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Run Tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.13 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.13 12 | id: go 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v1 15 | - name: Test 16 | run: go test -race -cover ./... 17 | -------------------------------------------------------------------------------- /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 2019 Brett Buddin 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 | # Fourier 2 | 3 | [![GoDoc](https://godoc.org/github.com/brettbuddin/fourier?status.svg)](https://godoc.org/github.com/brettbuddin/fourier) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/brettbuddin/fourier)](https://goreportcard.com/report/github.com/brettbuddin/fourier) 5 | 6 | - Fast Fourier Transform implementation via [Cooley-Tukey (Radix-2 DIT)](https://en.wikipedia.org/wiki/Cooley–Tukey_FFT_algorithm). 7 | - Convolution engine which performs partitioned convolution in the frequency domain using the [overlap-add method](https://en.wikipedia.org/wiki/Overlap–add_method). 8 | - Windowing functions for creating impulse responses. (e.g. Hann, Lanczos, etc) 9 | - Functions for creating common types of FIR filters. (e.g. low-pass, high-pass, etc) 10 | 11 | This library was written for use in a real-time audio context. `Convolver` 12 | allocates all of its buffers up-front and `Forward`/`Inverse` (FFT/IFFT) operate 13 | in-place. This is to avoid allocations in the hot-path. I've used this library 14 | to implement convolution reverb and perform various types of filtering. 15 | 16 | [Usage Examples](https://godoc.org/github.com/brettbuddin/fourier#pkg-examples) 17 | -------------------------------------------------------------------------------- /convolver.go: -------------------------------------------------------------------------------- 1 | package fourier 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | // Maximum impulse response length of 10 seconds at 96kHz 10 | const maxIRSamples = 20 * 96000 11 | 12 | // Convolver performs partioned convolution using the overlap-add method. It is 13 | // designed to convolve very long input streams with a FIR filter. 14 | type Convolver struct { 15 | // Sizes 16 | blockSize, fftSize int 17 | 18 | // Buffers 19 | inputSegments, responseSegments [][]complex128 20 | output, temp []complex128 21 | input, overlap []float64 22 | 23 | // Internal state 24 | inputSegmentPos, inputPos int 25 | channel, numChannels int 26 | } 27 | 28 | // NewConvolver returns a new Convolver. 29 | // 30 | // desiredBlockSize is maximum size consumed from the input (loaded into an input 31 | // segment) for each convolution operation (FFT -> multiply -> IFFT). Ideally, 32 | // it's maximum number of samples you plan on processing for each call to 33 | // Convolve. 34 | // 35 | // The length of the impulse response is limited to 1920000 samples (20s * 96kHz). 36 | // Exceeding this maximum length will result in truncation of the IR. 37 | func NewConvolver(desiredBlockSize int, ir []float64, opts ...ConvolverOption) (*Convolver, error) { 38 | if desiredBlockSize == 0 { 39 | return nil, errors.New("block size cannot be zero") 40 | } 41 | 42 | blockSize, fftSize := calcPartitionSize(desiredBlockSize) 43 | 44 | c := &Convolver{ 45 | blockSize: blockSize, 46 | fftSize: fftSize, 47 | numChannels: 1, 48 | input: make([]float64, fftSize), 49 | overlap: make([]float64, fftSize), 50 | output: make([]complex128, fftSize), 51 | temp: make([]complex128, fftSize), 52 | } 53 | 54 | for _, opt := range opts { 55 | if err := opt(c); err != nil { 56 | return c, err 57 | } 58 | } 59 | 60 | return c, c.SetImpulseResponse(ir) 61 | } 62 | 63 | // SetImpulseResponse sets the impulse response used in convolution. 64 | func (c *Convolver) SetImpulseResponse(ir []float64) error { 65 | if len(ir) == 0 { 66 | return errors.New("impulse response length cannot be zero") 67 | } 68 | 69 | var ( 70 | fftSize = c.fftSize 71 | blockSize = c.blockSize 72 | fillSize = fftSize - blockSize 73 | irSize = min(len(ir), maxIRSamples) 74 | numResponseSegments = int(irSize/fillSize + 1) 75 | ) 76 | 77 | numInputSegments := numResponseSegments 78 | if blockSize <= 128 { 79 | numInputSegments = 3 * numResponseSegments 80 | } 81 | 82 | // Allocate input segments 83 | inputSegments := make([][]complex128, numInputSegments) 84 | for i := range inputSegments { 85 | inputSegments[i] = make([]complex128, fftSize) 86 | } 87 | 88 | // Allocate frequency response segments 89 | responseSegments := make([][]complex128, numResponseSegments) 90 | for i := range responseSegments { 91 | responseSegments[i] = make([]complex128, fftSize) 92 | } 93 | loadIR(responseSegments, ir, fillSize) 94 | 95 | c.inputSegments = inputSegments 96 | c.responseSegments = responseSegments 97 | 98 | return nil 99 | } 100 | 101 | // Convolve convolves an a chunk of input against the loaded impulse response. 102 | func (c *Convolver) Convolve(out, in []float64, numSamples int) error { 103 | var ( 104 | numResponseSegments = len(c.responseSegments) 105 | numInputSegments = len(c.inputSegments) 106 | channel = c.channel 107 | numChannels = c.numChannels 108 | step = numInputSegments / numResponseSegments 109 | fftSize = c.fftSize 110 | blockSize = c.blockSize 111 | numSamplesProcessed = 0 112 | ) 113 | 114 | for numSamplesProcessed < numSamples { 115 | var ( 116 | numRemaining = numSamples - numSamplesProcessed 117 | blockLimit = blockSize - c.inputPos 118 | numSamplesToProcess = min(numRemaining, blockLimit) 119 | ) 120 | 121 | // Copy the input into the internal input buffer. If we've stepped 122 | // beyond the length of our input, leave zeros in the buffer. 123 | for i := 0; i < numSamplesToProcess; i++ { 124 | inIdx := channel + numSamplesProcessed + i*numChannels 125 | var v float64 126 | if inIdx <= len(in)-1 { 127 | v = in[inIdx] 128 | } 129 | c.input[c.inputPos+i] = v 130 | } 131 | inputSegment := c.inputSegments[c.inputSegmentPos] 132 | if err := cmplxCopyReal(inputSegment, c.input); err != nil { 133 | return err 134 | } 135 | 136 | // Forward FFT 137 | if err := Forward(inputSegment); err != nil { 138 | return err 139 | } 140 | 141 | // Multiply 142 | if c.inputPos == 0 { 143 | cmplxZero(c.temp) 144 | 145 | index := c.inputSegmentPos 146 | for i := 1; i < numResponseSegments; i++ { 147 | index += step 148 | if index >= numInputSegments { 149 | index -= numInputSegments 150 | } 151 | cmplxMultiplyAdd(c.temp, c.inputSegments[index], c.responseSegments[i]) 152 | } 153 | } 154 | 155 | if err := cmplxCopy(c.output, c.temp); err != nil { 156 | return err 157 | } 158 | if err := cmplxMultiplyAdd(c.output, inputSegment, c.responseSegments[0]); err != nil { 159 | return err 160 | } 161 | 162 | // Inverse FFT 163 | if err := Inverse(c.output); err != nil { 164 | return err 165 | } 166 | 167 | // Add overlap to the output 168 | for i := 0; i < numSamplesToProcess; i++ { 169 | var ( 170 | outIdx = numSamplesProcessed + channel + i*numChannels 171 | pos = c.inputPos + i 172 | ) 173 | // Guard against stepping outside the bounds of the output buffer 174 | if outIdx > len(out)-1 { 175 | continue 176 | } 177 | out[outIdx] = real(c.output[pos]) + c.overlap[pos] 178 | } 179 | 180 | c.inputPos += numSamplesToProcess 181 | 182 | if c.inputPos == blockSize { 183 | c.inputPos = 0 184 | zero(c.input) 185 | 186 | // Additional overlap when segment size > block size 187 | arErr := cmplxAddReal(c.output[blockSize:], c.overlap[blockSize:], fftSize-2*blockSize) 188 | if arErr != nil { 189 | return arErr 190 | } 191 | 192 | // Save the tail of the output as overlap 193 | for i := 0; i < fftSize-blockSize; i++ { 194 | c.overlap[i] = real(c.output[i+blockSize]) 195 | } 196 | 197 | // Step the current segment backwards 198 | if c.inputSegmentPos > 0 { 199 | c.inputSegmentPos-- 200 | } else { 201 | c.inputSegmentPos = numInputSegments - 1 202 | } 203 | } 204 | 205 | numSamplesProcessed += numSamplesToProcess 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // loadIR splits the impulse response into segments and transforms each segment 212 | // to the frequency domain to produce a partitioned frequency response. fillSize 213 | // specifies the number of samples of the IR that should be loaded into each 214 | // segment. 215 | func loadIR(segments [][]complex128, ir []float64, fillSize int) { 216 | for i := range segments { 217 | if i == 0 { 218 | segments[i][0] = complex(1, 0) 219 | } 220 | 221 | for j := 0; j < fillSize; j++ { 222 | irIdx := j + i*(fillSize) 223 | if irIdx < len(ir) { 224 | v := ir[irIdx] 225 | if math.IsNaN(v) { 226 | v = 0 227 | } 228 | segments[i][j] = complex(v, 0) 229 | } 230 | } 231 | 232 | Forward(segments[i]) 233 | } 234 | } 235 | 236 | // cmplxMultiplyAdd multiplies two complex buffers and adds the result to another. 237 | func cmplxMultiplyAdd(dest, a, b []complex128) error { 238 | var ( 239 | la = len(a) 240 | lb = len(b) 241 | ldest = len(dest) 242 | ) 243 | if ldest != la || ldest != lb { 244 | return fmt.Errorf("buffer sizes do not match: dest=%d a=%d b=%d", ldest, la, lb) 245 | } 246 | for i := range dest { 247 | dest[i] += a[i] * b[i] 248 | } 249 | return nil 250 | } 251 | 252 | // cmplxCopy copies one complex buffer into another. 253 | func cmplxCopy(dest, src []complex128) error { 254 | var ( 255 | ldest = len(dest) 256 | lsrc = len(src) 257 | ) 258 | if ldest != lsrc { 259 | return fmt.Errorf("buffer lengths do not match: dest=%d src=%d", ldest, lsrc) 260 | } 261 | for i := range src { 262 | dest[i] = src[i] 263 | } 264 | return nil 265 | } 266 | 267 | // cmplxCopyReal copies a real buffer to a complex buffer. The imaginary 268 | // component is set to zero. 269 | func cmplxCopyReal(dest []complex128, src []float64) error { 270 | var ( 271 | ldest = len(dest) 272 | lsrc = len(src) 273 | ) 274 | if ldest != lsrc { 275 | return fmt.Errorf("buffer lengths do not match: dest=%d src=%d", ldest, lsrc) 276 | } 277 | for i := range src { 278 | dest[i] = complex(src[i], 0) 279 | } 280 | return nil 281 | } 282 | 283 | // cmplxAddReal adds a real buffer to a complex buffer's real component. 284 | func cmplxAddReal(dest []complex128, vals []float64, size int) error { 285 | var ( 286 | ldest = len(dest) 287 | lvals = len(vals) 288 | ) 289 | if size > ldest || size > lvals { 290 | return fmt.Errorf("operation size larger than buffers: dest=%d vals=%d size=%d", ldest, lvals, size) 291 | } 292 | for i := range vals { 293 | dest[i] += complex(vals[i], imag(dest[i])) 294 | } 295 | return nil 296 | } 297 | 298 | // cmplxZero zeros out a complex buffer. 299 | func cmplxZero(dest []complex128) { 300 | for i := range dest { 301 | dest[i] = complex(0, 0) 302 | } 303 | } 304 | 305 | // zero zeros out a buffer. 306 | func zero(dest []float64) { 307 | for i := range dest { 308 | dest[i] = 0 309 | } 310 | } 311 | 312 | func min(a, b int) int { 313 | if a < b { 314 | return a 315 | } 316 | return b 317 | } 318 | 319 | // calcPartitionSize calculates the appropriate size of the partitions used in 320 | // convolution. It returns two values: a block size that's quantized (up) to the 321 | // nearest power of two and the FFT size we should use. 322 | func calcPartitionSize(desiredblockSize int) (blockSize int, fftSize int) { 323 | blockSize = nextPowerOfTwo(desiredblockSize) 324 | if blockSize <= 128 { 325 | return blockSize, 4 * blockSize 326 | } 327 | return blockSize, 2 * blockSize 328 | } 329 | 330 | // ConvolverOption is a configuration option for Convolver. 331 | type ConvolverOption func(*Convolver) error 332 | 333 | // ForChannel configures a Convolver to target a specific channel when the input 334 | // buffer contains multiple interleaved channels. 335 | func ForChannel(channel, numChannels int) ConvolverOption { 336 | return func(c *Convolver) error { 337 | if channel < 0 { 338 | return errors.New("channel cannot be negative") 339 | } 340 | if numChannels < 1 { 341 | return errors.New("number of channels cannot be less than 1") 342 | } 343 | if channel >= numChannels { 344 | return errors.New("channel out of range of total number of channels") 345 | } 346 | c.channel = channel 347 | c.numChannels = numChannels 348 | return nil 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /convolver_test.go: -------------------------------------------------------------------------------- 1 | package fourier 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/brettbuddin/fourier/filter" 9 | "github.com/brettbuddin/fourier/window" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestConvolution_SmallerImpulse(t *testing.T) { 14 | var ( 15 | blockSize = 8 16 | impulseSize = 4 17 | 18 | impulse = make([]float64, impulseSize) 19 | input = make([]float64, blockSize) 20 | output = make([]float64, blockSize) 21 | ) 22 | 23 | for i := range impulse { 24 | impulse[i] = 1 25 | } 26 | 27 | for i := range input { 28 | input[i] = float64(i) + 1 29 | } 30 | 31 | convolver, err := NewConvolver(blockSize, impulse) 32 | require.NoError(t, err) 33 | 34 | cErr := convolver.Convolve(output, input, blockSize) 35 | require.NoError(t, cErr) 36 | 37 | expected := []float64{1, 3, 6, 10, 14, 18, 22, 26} 38 | 39 | require.InEpsilonSlice(t, expected, output, epsilon) 40 | } 41 | 42 | func TestConvolution_LargerImpulse(t *testing.T) { 43 | var ( 44 | blockSize = 8 45 | inputSize = 8 46 | impulseSize = 256 47 | 48 | impulse = make([]float64, impulseSize) 49 | input = make([]float64, inputSize) 50 | output = make([]float64, inputSize+impulseSize-1) 51 | ) 52 | 53 | for i := range impulse { 54 | impulse[i] = 1 55 | } 56 | impulse[len(impulse)-1] = 2 57 | 58 | for i := range input { 59 | input[i] = 1 60 | } 61 | 62 | convolver, err := NewConvolver(blockSize, impulse) 63 | require.NoError(t, err) 64 | 65 | // Convolve in blocks, comparing the output of each block to an expected 66 | // value. The last value of the convolution will be different in delta from 67 | // all the rest. 68 | for i := 0; i < len(output); i += blockSize { 69 | var ( 70 | inBegin = min(i, len(input)) 71 | inEnd = min(i+blockSize, len(input)) 72 | outEnd = min(i+blockSize, len(output)) 73 | ) 74 | cErr := convolver.Convolve(output[i:outEnd], input[inBegin:inEnd], blockSize) 75 | require.NoError(t, cErr) 76 | } 77 | 78 | expected := []float64{1, 2, 3, 4, 5, 6, 7} 79 | for j := 0; j < 248; j++ { 80 | expected = append(expected, 8) 81 | } 82 | expected = append(expected, []float64{9, 8, 7, 6, 5, 4, 3, 2}...) 83 | require.InEpsilonSlice(t, expected, output, epsilon) 84 | } 85 | 86 | func TestConvolution_PartialOfMaxBlockSize(t *testing.T) { 87 | var ( 88 | partialSize = 4 89 | blockSize = 8 90 | impulseSize = 16 91 | 92 | impulse = make([]float64, impulseSize) 93 | input = make([]float64, partialSize) 94 | output = make([]float64, partialSize) 95 | ) 96 | 97 | for i := range impulse { 98 | impulse[i] = 1 99 | } 100 | 101 | for i := range input { 102 | input[i] = 1 103 | } 104 | 105 | convolver, err := NewConvolver(blockSize, impulse) 106 | require.NoError(t, err) 107 | 108 | cErr := convolver.Convolve(output, input, partialSize) 109 | require.NoError(t, cErr) 110 | 111 | expected := []float64{1, 2, 3, 4} 112 | require.InEpsilonSlice(t, expected, output, epsilon) 113 | } 114 | 115 | func TestConvolver_ErroneousCreation(t *testing.T) { 116 | _, lErr := NewConvolver(0, []float64{1}) 117 | require.Error(t, lErr) 118 | 119 | _, irErr := NewConvolver(64, []float64{}) 120 | require.Error(t, irErr) 121 | } 122 | 123 | func BenchmarkConvolver(b *testing.B) { 124 | var ( 125 | blockSize = 64 126 | ir = make([]float64, 500) 127 | in = make([]float64, blockSize) 128 | out = make([]float64, blockSize) 129 | ) 130 | 131 | conv, _ := NewConvolver(blockSize, ir) 132 | 133 | b.ReportAllocs() 134 | b.ResetTimer() 135 | for i := 0; i < b.N; i++ { 136 | conv.Convolve(out, in, blockSize) 137 | } 138 | } 139 | 140 | func TestConvolution_Interleaved(t *testing.T) { 141 | var ( 142 | blockSize = 8 143 | impulseSize = 4 144 | numChannels = 2 145 | 146 | impulse = make([]float64, impulseSize) 147 | input = make([]float64, numChannels*blockSize) 148 | output = make([]float64, numChannels*blockSize) 149 | ) 150 | 151 | for i := range impulse { 152 | impulse[i] = 1 153 | } 154 | 155 | for i := 0; i < len(input); i += numChannels { 156 | input[i] = float64(i/numChannels) + 1 157 | input[i+1] = 2 * input[i] 158 | } 159 | 160 | // Create a convolver for each channel and convolve that channel's input 161 | for i := 0; i < numChannels; i++ { 162 | conv, err := NewConvolver(blockSize, impulse, ForChannel(i, numChannels)) 163 | require.NoError(t, err) 164 | 165 | cErr := conv.Convolve(output, input, blockSize) 166 | require.NoError(t, cErr) 167 | } 168 | 169 | expected := []float64{1, 2, 3, 6, 6, 12, 10, 20, 14, 28, 18, 36, 22, 44, 26, 52} 170 | 171 | require.InEpsilonSlice(t, expected, output, epsilon) 172 | } 173 | 174 | func ExampleConvolver_simple() { 175 | var ( 176 | blockSize = 8 177 | ir = []float64{1, 1} 178 | conv, _ = NewConvolver(blockSize, ir) 179 | in = []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 180 | out = make([]float64, len(in)+len(ir)-1) 181 | ) 182 | 183 | // Convolve the entire input buffer. We're using the output buffer length 184 | // here in order to capture the full convolved output of INPUT_LENGTH + 185 | // IR_LENGTH - 1. Convolving more samples will result in the values 186 | // eventually dropping to zero. 187 | _ = conv.Convolve(out, in, len(out)) 188 | 189 | // Round to nearest integer (removing error) for pretty printing 190 | roundTo(out, 1e10) 191 | 192 | fmt.Println(out) 193 | // Output: [1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 16] 194 | } 195 | 196 | func ExampleConvolver_chunks() { 197 | var ( 198 | blockSize = 8 199 | ir = []float64{1, 1} 200 | conv, _ = NewConvolver(blockSize, ir) 201 | in = []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 202 | out = make([]float64, len(in)+len(ir)-1) 203 | ) 204 | 205 | for i := 0; i < len(out); i += blockSize { 206 | var ( 207 | inBegin = min(i, len(in)) 208 | inEnd = min(i+blockSize, len(in)) 209 | outEnd = min(i+blockSize, len(out)) 210 | 211 | inChunk = in[inBegin:inEnd] 212 | outChunk = out[i:outEnd] 213 | ) 214 | 215 | // We are deriving the input and output chunks here, but they would be 216 | // presented to you via the callback mechanisms in a streaming audio 217 | // scenario. The algorithm is able to accomodate this chunking. 218 | _ = conv.Convolve(outChunk, inChunk, blockSize) 219 | } 220 | 221 | // Round to nearest integer (removing error) for pretty printing 222 | roundTo(out, 1e10) 223 | 224 | fmt.Println(out) 225 | // Output: [1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 16] 226 | } 227 | 228 | func ExampleConvolver_filtering() { 229 | var ( 230 | blockSize = 256 231 | sampleRate = 320.0 232 | cutoff = 30.0 233 | kernel = make([]float64, 32) 234 | in = make([]float64, blockSize) 235 | ) 236 | 237 | // Sum two cosine waves, one at 10Hz and another at 90Hz 238 | carrier(in, 10.0, sampleRate) 239 | carrier(in, 90.0, sampleRate) 240 | 241 | // Calculate bins with high magnitude before filtering 242 | spikesBefore := detectSpikes(in) 243 | 244 | // Build a filter kernel that filters frequencies higher than 30Hz at 320Hz 245 | // sampling rate and convolve the summed signal with it. 246 | filter.MakeLowPass(kernel, window.Lanczos, cutoff/sampleRate) 247 | conv, _ := NewConvolver(blockSize, kernel) 248 | 249 | out := make([]float64, blockSize) 250 | conv.Convolve(out, in, len(out)) 251 | 252 | // Calculate bins with high magnitude after filtering 253 | spikesAfter := detectSpikes(out) 254 | 255 | fmt.Println("spikes at (before):", spikesBefore) 256 | fmt.Println("spikes at (after):", spikesAfter) 257 | // Output: spikes at (before): [8 72 184 248] 258 | // spikes at (after): [8 248] 259 | } 260 | 261 | func magnitude(src []float64) []float64 { 262 | dest := make([]float64, len(src)) 263 | freq := make([]complex128, len(src)) 264 | for i, v := range src { 265 | freq[i] = complex(v, 0) 266 | } 267 | Forward(freq) 268 | Magnitude(dest, freq) 269 | return dest 270 | } 271 | 272 | func detectSpikes(buf []float64) []int { 273 | var spikes []int 274 | for i, v := range magnitude(buf) { 275 | if v > 0.2 { 276 | spikes = append(spikes, i) 277 | } 278 | } 279 | return spikes 280 | } 281 | 282 | func carrier(dest []float64, fc, fs float64) { 283 | for i := 0; i < len(dest); i++ { 284 | dest[i] += math.Cos((float64(i) * 2 * math.Pi * fc) / fs) 285 | } 286 | } 287 | 288 | func cmplxCarrier(dest []complex128, fc, fs float64) { 289 | destf := make([]float64, len(dest)) 290 | carrier(destf, fc, fs) 291 | for i := range dest { 292 | dest[i] = complex(destf[i], 0) 293 | } 294 | } 295 | 296 | func roundTo(out []float64, epsilon float64) { 297 | for i := range out { 298 | out[i] = math.Round(out[i]*epsilon) / epsilon 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /fft.go: -------------------------------------------------------------------------------- 1 | // Package fourier provides a Fast Fourier Transform implementation and 2 | // Convolver that performs partioned convolution in the frequency domain. 3 | package fourier 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "math/cmplx" 9 | ) 10 | 11 | // Forward performs a forward FFT via Cooley-Tukey Radix-2 DIT. The buffer 12 | // length is required to be a power of two. 13 | func Forward(v []complex128) error { 14 | return forward(v) 15 | } 16 | 17 | // Inverse performs an inverse FFT via Cooley-Tukey Radix-2 DIT. The buffer 18 | // length is required to be a power of two. 19 | func Inverse(v []complex128) error { 20 | for i := range v { 21 | v[i] = cmplx.Conj(v[i]) 22 | } 23 | if err := forward(v); err != nil { 24 | return err 25 | } 26 | cmplxNormalize(v, len(v)) 27 | return nil 28 | } 29 | 30 | // Magnitude calculates the normalized magnitude of a frequency-domain signal. 31 | // Each bin represents the magnitude of a specific frequency in the input. Use 32 | // the first half of the output buffer for a traditional frequency content view 33 | // (Nyquist is at N/2). 34 | // 35 | // Pop! Pop! 36 | func Magnitude(dest []float64, src []complex128) error { 37 | if len(dest) != len(src) { 38 | return fmt.Errorf("source and destination slices not the same size: dest=%d src=%d", len(dest), len(src)) 39 | } 40 | for i := 0; i < len(src); i++ { 41 | dest[i] = cmplx.Abs(src[i]) 42 | } 43 | 44 | normalize(dest, len(dest)/2) 45 | 46 | return nil 47 | } 48 | 49 | // forward performs a forward FFT. 50 | func forward(v []complex128) error { 51 | n := len(v) 52 | if n == 2 { 53 | return nil 54 | } 55 | 56 | if !isPowerOfTwo(n) { 57 | return errors.New("buffer length is not a power of two") 58 | } 59 | 60 | table := twiddleTable(n) 61 | 62 | // Reorder the input in preparation for the Butterfly 63 | reorder(v) 64 | 65 | // Perform butterfly 66 | for size := 2; size <= n; size *= 2 { 67 | var ( 68 | half = size / 2 69 | step = n / size 70 | ) 71 | 72 | for i := 0; i < n; i += size { 73 | var ( 74 | j = i 75 | k = 0 76 | ) 77 | for j < i+half { 78 | var ( 79 | l = j + half 80 | cos = table.cos[k] 81 | sin = table.sin[k] 82 | twiddle = complex(real(v[l])*cos+imag(v[l])*sin, -real(v[l])*sin+imag(v[l])*cos) 83 | ) 84 | 85 | v[l] = v[j] - twiddle 86 | v[j] += twiddle 87 | 88 | j++ 89 | k += step 90 | } 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // reorder reorders a complex buffer's values to form the pattern necessary for 97 | // the Cooley-Tukey radix-2 DIT butterfly operation. 98 | func reorder(v []complex128) { 99 | var ( 100 | lv = uint(len(v)) 101 | bits = log2(lv) 102 | i uint 103 | ) 104 | for ; i < lv; i++ { 105 | j := reverseBits(i, bits) 106 | if j > i { 107 | v[j], v[i] = v[i], v[j] 108 | } 109 | } 110 | } 111 | 112 | // log2 returns log base-2 of an integer 113 | func log2(v uint) uint { 114 | var r uint 115 | for v >>= 1; v != 0; v >>= 1 { 116 | r++ 117 | } 118 | return r 119 | } 120 | 121 | // reverseBits reverses all bits up until a designated place significance. 122 | func reverseBits(v, bits uint) uint { 123 | if bits < 2 { 124 | return v 125 | } 126 | 127 | var r uint = v & 1 128 | bits-- 129 | 130 | for v >>= 1; v != 0; v >>= 1 { 131 | r <<= 1 132 | r |= v & 1 133 | bits-- 134 | } 135 | return r << bits 136 | } 137 | 138 | // isPowerOfTwo determines whether or not an integer is a power of two. 139 | func isPowerOfTwo(v int) bool { 140 | return (v != 0) && (v&(v-1)) == 0 141 | } 142 | 143 | // nextPowerOfTwo finds the next power of two above the value provided. 144 | func nextPowerOfTwo(v int) int { 145 | if isPowerOfTwo(v) { 146 | return v 147 | } 148 | 149 | v-- 150 | v |= v >> 1 151 | v |= v >> 2 152 | v |= v >> 4 153 | v |= v >> 8 154 | v |= v >> 16 155 | v++ 156 | return v 157 | } 158 | 159 | // cmplxNormalize proportions the values to the length of the buffer 160 | func cmplxNormalize(v []complex128, size int) { 161 | scale := 1 / float64(size) 162 | for i := range v { 163 | v[i] = complex(real(v[i])*scale, 0) 164 | } 165 | } 166 | 167 | // normalize proportions the values to the length of the buffer 168 | func normalize(v []float64, size int) { 169 | scale := 1 / float64(size) 170 | for i := range v { 171 | v[i] = v[i] * scale 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /fft_test.go: -------------------------------------------------------------------------------- 1 | package fourier 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var epsilon = 0.0000001 13 | 14 | func TestBitReversal(t *testing.T) { 15 | b16 := "%016b" 16 | require.Equal(t, "0000000000000100", fmt.Sprintf(b16, reverseBits(2, 4))) 17 | require.Equal(t, "0000000001000000", fmt.Sprintf(b16, reverseBits(2, 8))) 18 | require.Equal(t, "0000000010000000", fmt.Sprintf(b16, reverseBits(1, 8))) 19 | require.Equal(t, "0000000000100000", fmt.Sprintf(b16, reverseBits(1, 6))) 20 | require.Equal(t, "1000000000000000", fmt.Sprintf(b16, reverseBits(1, 16))) 21 | require.Equal(t, "0000000000000001", fmt.Sprintf(b16, reverseBits(1, 1))) 22 | require.Equal(t, "0000000000000001", fmt.Sprintf(b16, reverseBits(1, 0))) 23 | } 24 | 25 | func TestLog2(t *testing.T) { 26 | require.Equal(t, uint(1), log2(3)) 27 | require.Equal(t, uint(4), log2(16)) 28 | require.Equal(t, uint(5), log2(32)) 29 | } 30 | 31 | func TestIsPowerOf2(t *testing.T) { 32 | require.True(t, isPowerOfTwo(2)) 33 | require.True(t, isPowerOfTwo(4)) 34 | require.True(t, isPowerOfTwo(8)) 35 | require.True(t, isPowerOfTwo(1)) 36 | require.False(t, isPowerOfTwo(5)) 37 | require.False(t, isPowerOfTwo(7)) 38 | } 39 | 40 | func TestNextPowerOfTwo(t *testing.T) { 41 | require.Equal(t, 1, nextPowerOfTwo(1)) 42 | require.Equal(t, 8, nextPowerOfTwo(5)) 43 | require.Equal(t, 64, nextPowerOfTwo(50)) 44 | require.Equal(t, 64, nextPowerOfTwo(64)) 45 | } 46 | 47 | func TestButterflyReorder(t *testing.T) { 48 | buf := []complex128{ 49 | complex(1, 0), 50 | complex(2, 0), 51 | complex(3, 0), 52 | complex(4, 0), 53 | complex(5, 0), 54 | complex(6, 0), 55 | complex(7, 0), 56 | complex(8, 0), 57 | } 58 | 59 | reorder(buf) 60 | 61 | require.Equal(t, []complex128{ 62 | complex(1, 0), 63 | complex(5, 0), 64 | complex(3, 0), 65 | complex(7, 0), 66 | complex(2, 0), 67 | complex(6, 0), 68 | complex(4, 0), 69 | complex(8, 0), 70 | }, buf) 71 | } 72 | 73 | func TestForwardTransform(t *testing.T) { 74 | buf := make([]complex128, 8) 75 | for i := 0; i < 4; i++ { 76 | buf[i] = complex(1, 0) 77 | } 78 | 79 | Forward(buf) 80 | cmplxEqualEpsilon(t, []complex128{ 81 | (4 + 0i), 82 | (1 - 2.414213562373095i), 83 | (0 + 0i), 84 | (1 - 0.4142135623730949i), 85 | (0 + 0i), 86 | (0.9999999999999999 + 0.4142135623730949i), 87 | (0 + 0i), 88 | (0.9999999999999997 + 2.414213562373095i), 89 | }, buf, epsilon) 90 | } 91 | 92 | func TestRoundTripTransform(t *testing.T) { 93 | buf := make([]complex128, 4) 94 | for i := range buf { 95 | buf[i] = complex(float64(i)+1, 0) 96 | } 97 | 98 | Forward(buf) 99 | cmplxEqualEpsilon(t, []complex128{ 100 | (10 + 0i), 101 | (-2 + 2i), 102 | (-2 + 0i), 103 | (-2 - 2i), 104 | }, buf, epsilon) 105 | 106 | Inverse(buf) 107 | cmplxEqualEpsilon(t, []complex128{ 108 | (1 + 0i), 109 | (2 + 0i), 110 | (3 + 0i), 111 | (4 + 0i), 112 | }, buf, epsilon) 113 | } 114 | 115 | func TestFrequencyDomainZeroPaddingResample(t *testing.T) { 116 | for _, tt := range []struct { 117 | src []float64 118 | scale int 119 | expected []float64 120 | }{ 121 | { 122 | src: []float64{1, 0.5, 1.0, 0.5, 1.0, 0.5, 1.0, 0.5}, 123 | scale: 1, 124 | expected: []float64{1, 0.5, 1.0, 0.5, 1.0, 0.5, 1.0, 0.5}, 125 | }, 126 | { 127 | src: []float64{1, 0.5, 1.0, 0.5, 1.0, 0.5, 1.0, 0.5}, 128 | scale: 2, 129 | expected: []float64{1, 0.75, 0.5, 0.75, 1.0, 0.75, 0.5, 0.75, 1.0, 0.75, 0.5, 0.75, 1.0, 0.75, 0.5, 0.75}, 130 | }, 131 | { 132 | src: []float64{1, 0.5, 1.0, 0.5, 1.0, 0.5, 1.0, 0.5}, 133 | scale: 8, 134 | expected: []float64{1, 0.9809698831278217, 0.9267766952966369, 0.8456708580912724, 0.75, 0.6543291419087276, 0.5732233047033631, 0.5190301168721783, 0.5, 0.5190301168721783, 0.5732233047033631, 0.6543291419087276, 0.75, 0.8456708580912724, 0.9267766952966369, 0.9809698831278217, 1, 0.9809698831278217, 0.9267766952966369, 0.8456708580912724, 0.75, 0.6543291419087276, 0.5732233047033631, 0.5190301168721783, 0.5, 0.5190301168721783, 0.5732233047033631, 0.6543291419087276, 0.75, 0.8456708580912724, 0.9267766952966369, 0.9809698831278217, 1, 0.9809698831278217, 0.9267766952966369, 0.8456708580912724, 0.75, 0.6543291419087276, 0.5732233047033631, 0.5190301168721783, 0.5, 0.5190301168721783, 0.5732233047033631, 0.6543291419087276, 0.75, 0.8456708580912724, 0.9267766952966369, 0.9809698831278217, 1, 0.9809698831278217, 0.9267766952966369, 0.8456708580912724, 0.75, 0.6543291419087276, 0.5732233047033631, 0.5190301168721783, 0.5, 0.5190301168721783, 0.5732233047033631, 0.6543291419087276, 0.75, 0.8456708580912724, 0.9267766952966369, 0.9809698831278217}, 135 | }, 136 | } { 137 | var ( 138 | l = len(tt.src) 139 | ln = l * tt.scale 140 | csrc = make([]complex128, l) 141 | cpadded = make([]complex128, ln) 142 | out = make([]float64, ln) 143 | ) 144 | 145 | for i, v := range tt.src { 146 | csrc[i] = complex(v*float64(tt.scale), 0) 147 | } 148 | require.NoError(t, Forward(csrc)) 149 | 150 | // Split the spectral content down the middle and copy both ends into the 151 | // ends of a new upscaled buffer. This will leave the zeros in the center. 152 | for i := 0; i < l/2; i++ { 153 | cpadded[i] = csrc[i] 154 | cpadded[len(cpadded)-1-i] = csrc[len(csrc)-1-i] 155 | } 156 | require.NoError(t, Inverse(cpadded)) 157 | for i, v := range cpadded { 158 | out[i] = real(v) 159 | } 160 | 161 | assert.Equal(t, tt.expected, out) 162 | } 163 | } 164 | 165 | func TestMagnitude(t *testing.T) { 166 | const fc = 10.0 167 | const fs = 32.0 * fc 168 | const size = 256 169 | 170 | carrier := make([]complex128, size) 171 | cmplxCarrier(carrier, fc, fs) 172 | 173 | err := Forward(carrier) 174 | require.NoError(t, err) 175 | 176 | abs := make([]float64, len(carrier)) 177 | err = Magnitude(abs, carrier) 178 | require.NoError(t, err) 179 | 180 | // Carrier Frequency = 10Hz 181 | // Resolution = 1.25Hz 182 | // Spike @ Carrier Frequency / Resolution = 8 183 | assert.Equal(t, 1.0, abs[8]) 184 | 185 | // Spike in negative frequencies 186 | assert.Equal(t, 1.0, abs[248]) 187 | 188 | // Other parts should be close to zero 189 | assert.Equal(t, 0.0, math.Round(abs[0])) 190 | assert.Equal(t, 0.0, math.Round(abs[10])) 191 | 192 | } 193 | 194 | func cmplxEqualEpsilon(t *testing.T, expected, actual []complex128, epsilon float64) { 195 | t.Helper() 196 | 197 | for i := range expected { 198 | if real(expected[i]) == 0 { 199 | assert.Equal(t, 0.0, real(actual[i])) 200 | } else { 201 | assert.InEpsilon(t, real(expected[i]), real(actual[i]), epsilon) 202 | } 203 | 204 | if imag(expected[i]) == 0 { 205 | assert.Equal(t, 0.0, imag(actual[i])) 206 | } else { 207 | assert.InEpsilon(t, imag(expected[i]), imag(actual[i]), epsilon) 208 | } 209 | } 210 | } 211 | 212 | func BenchmarkFFT(b *testing.B) { 213 | b.ReportAllocs() 214 | b.StopTimer() 215 | src := make([]complex128, 64) 216 | for i := range src { 217 | src[i] = complex(float64(i)+1, 0) 218 | } 219 | 220 | buf := make([]complex128, len(src)) 221 | for i := 0; i < b.N; i++ { 222 | copy(buf, src) 223 | b.StartTimer() 224 | Forward(buf) 225 | b.StopTimer() 226 | } 227 | } 228 | 229 | func BenchmarkIFFT(b *testing.B) { 230 | b.ReportAllocs() 231 | b.StopTimer() 232 | src := make([]complex128, 64) 233 | for i := range src { 234 | src[i] = complex(float64(i)+1, 0) 235 | } 236 | Forward(src) 237 | 238 | buf := make([]complex128, len(src)) 239 | for i := 0; i < b.N; i++ { 240 | copy(buf, src) 241 | b.StartTimer() 242 | Inverse(buf) 243 | b.StopTimer() 244 | } 245 | } 246 | 247 | func ExampleForward_roundtrip() { 248 | buf := make([]complex128, 8) 249 | for i := range buf { 250 | buf[i] = complex(float64(i+1), 0) 251 | } 252 | fmt.Println("time:", buf) 253 | 254 | // Transform to the frequency domain 255 | Forward(buf) 256 | fmt.Println("frequency:", buf) 257 | 258 | // Transform back to the time domain 259 | Inverse(buf) 260 | fmt.Println("time:", buf) 261 | 262 | // Output: time: [(1+0i) (2+0i) (3+0i) (4+0i) (5+0i) (6+0i) (7+0i) (8+0i)] 263 | // frequency: [(36+0i) (-4+9.65685424949238i) (-4+4i) (-4+1.6568542494923797i) (-4+0i) (-3.9999999999999996-1.6568542494923797i) (-3.9999999999999996-4i) (-3.9999999999999987-9.65685424949238i)] 264 | // time: [(1.0000000000000002+0i) (2+0i) (3+0i) (4+0i) (5+0i) (6+0i) (7+0i) (8+0i)] 265 | } 266 | -------------------------------------------------------------------------------- /filter/filter.go: -------------------------------------------------------------------------------- 1 | // Package filter provides builders for designing kernels for common filter 2 | // types. 3 | package filter 4 | 5 | import ( 6 | "math" 7 | 8 | "github.com/brettbuddin/fourier/window" 9 | ) 10 | 11 | // MakeLowPass creates a low-pass filter impulse response. It filters 12 | // frequencies higher than the cutoff frequency. 13 | func MakeLowPass(h []float64, wf window.Func, cutoff float64) { 14 | n := len(h) 15 | for i := range h { 16 | x := 2 * math.Pi * cutoff 17 | if i == n/2 { 18 | h[i] = x 19 | } else { 20 | y := float64(i) - float64(n)/2 21 | h[i] = (math.Sin(x*y) / y) * wf(float64(i), n) 22 | } 23 | } 24 | normalize(h) 25 | } 26 | 27 | // MakeHighPass creates a high-pass filter impulse response. It filters 28 | // frequencies lower than the cutoff frequency. 29 | func MakeHighPass(h []float64, wf window.Func, cutoff float64) { 30 | MakeLowPass(h, wf, cutoff) 31 | for i := range h { 32 | h[i] = -h[i] 33 | } 34 | } 35 | 36 | // MakeBandReject creates a band-reject filter impulse response. It filters out 37 | // frequencies between the two stop frequencies. 38 | func MakeBandReject(h []float64, wf window.Func, stop1, stop2 float64) { 39 | a := make([]float64, len(h)) 40 | b := make([]float64, len(h)) 41 | MakeLowPass(a, wf, stop1) 42 | MakeHighPass(b, wf, stop2) 43 | for i := range h { 44 | h[i] = a[i] + b[i] 45 | } 46 | } 47 | 48 | // MakeBandPass creates a band-pass filter impulse response. It allows 49 | // frequences between the two stop frequencies. 50 | func MakeBandPass(h []float64, wf window.Func, stop1, stop2 float64) { 51 | MakeBandReject(h, wf, stop1, stop2) 52 | for i := range h { 53 | h[i] = -h[i] 54 | } 55 | } 56 | 57 | func normalize(w []float64) { 58 | var sum float64 59 | for i := range w { 60 | sum += w[i] 61 | } 62 | scale := 1.0 / sum 63 | for i := range w { 64 | w[i] *= scale 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brettbuddin/fourier/window" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLowPass(t *testing.T) { 11 | kernel := make([]float64, 10) 12 | MakeLowPass(kernel, window.Blackman, 0.5) 13 | 14 | expected := []float64{ 15 | -5.409800153010306e-34, 16 | -1.5675664736656203e-18, 17 | 7.826365172768749e-18, 18 | -1.9872378605190914e-17, 19 | 3.310443906868464e-17, 20 | 1, 21 | 3.310443906868464e-17, 22 | -1.9872378605190917e-17, 23 | 7.826365172768752e-18, 24 | -1.567566473665619e-18, 25 | } 26 | require.InEpsilonSlice(t, expected, kernel, 1e-10) 27 | } 28 | 29 | func TestHighPass(t *testing.T) { 30 | kernel := make([]float64, 10) 31 | MakeHighPass(kernel, window.Blackman, 0.5) 32 | 33 | expected := []float64{ 34 | 5.409800153010306e-34, 35 | 1.5675664736656203e-18, 36 | -7.826365172768749e-18, 37 | 1.9872378605190914e-17, 38 | -3.310443906868464e-17, 39 | -1, 40 | -3.310443906868464e-17, 41 | 1.9872378605190917e-17, 42 | -7.826365172768752e-18, 43 | 1.567566473665619e-18, 44 | } 45 | require.InEpsilonSlice(t, expected, kernel, 1e-10) 46 | } 47 | 48 | func TestBandPass(t *testing.T) { 49 | kernel := make([]float64, 10) 50 | MakeBandPass(kernel, window.Blackman, 0.25, 0.5) 51 | 52 | expected := []float64{ 53 | 8.852297468639933e-19, 54 | -7.822375291978086e-19, 55 | 0.02134438446523165, 56 | -2.982816317717476e-17, 57 | -0.27085135668587773, 58 | 0.49901394444129243, 59 | -0.27085135668587773, 60 | -2.982816317717477e-17, 61 | 0.021344384465231663, 62 | -7.822375291978078e-19, 63 | } 64 | require.InEpsilonSlice(t, expected, kernel, 1e-10) 65 | } 66 | 67 | func TestBandReject(t *testing.T) { 68 | kernel := make([]float64, 10) 69 | MakeBandReject(kernel, window.Blackman, 0.25, 0.5) 70 | 71 | expected := []float64{ 72 | -8.852297468639933e-19, 73 | 7.822375291978086e-19, 74 | -0.02134438446523165, 75 | 2.982816317717476e-17, 76 | 0.27085135668587773, 77 | -0.49901394444129243, 78 | 0.27085135668587773, 79 | 2.982816317717477e-17, 80 | -0.021344384465231663, 81 | 7.822375291978078e-19, 82 | } 83 | require.InEpsilonSlice(t, expected, kernel, 1e-10) 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brettbuddin/fourier 2 | 3 | go 1.12 4 | 5 | require github.com/stretchr/testify v1.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | -------------------------------------------------------------------------------- /twiddle.go: -------------------------------------------------------------------------------- 1 | package fourier 2 | 3 | import "math" 4 | 5 | // table is a trigonometric "twiddle" table. 6 | type table struct { 7 | sin, cos []float64 8 | } 9 | 10 | var twiddleTables = map[int]*table{} 11 | 12 | // twiddleTable looks up a twiddle table for a particular FFT size. If the table 13 | // has already been calculated, a cached version is returned. 14 | func twiddleTable(size int) *table { 15 | if _, ok := twiddleTables[size]; ok { 16 | return twiddleTables[size] 17 | } 18 | t := &table{ 19 | cos: make([]float64, size/2), 20 | sin: make([]float64, size/2), 21 | } 22 | for i := 0; i < size/2; i++ { 23 | fi := float64(i) 24 | fsize := float64(size) 25 | t.cos[i] = math.Cos(2 * math.Pi * fi / fsize) 26 | t.sin[i] = math.Sin(2 * math.Pi * fi / fsize) 27 | } 28 | twiddleTables[size] = t 29 | return t 30 | } 31 | -------------------------------------------------------------------------------- /window/window.go: -------------------------------------------------------------------------------- 1 | // Package window provides various windowing functions for use in designing FIR 2 | // filters. 3 | package window 4 | 5 | import "math" 6 | 7 | // Func is a windowing function. 8 | type Func func(x float64, n int) float64 9 | 10 | // Blackman is a Blackman windowing function. 11 | // 12 | // Reference: https://en.wikipedia.org/wiki/Window_function#Blackman_window 13 | func Blackman(x float64, n int) float64 { 14 | return 0.42 - (0.5 * math.Cos((2*math.Pi*x)/float64(n))) + (0.08 * math.Cos((4*math.Pi*x)/float64(n))) 15 | } 16 | 17 | // Hann is a Hann windowing function. 18 | // 19 | // Reference: https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows 20 | func Hann(x float64, n int) float64 { 21 | return hannHamming(0.5, x, n) 22 | } 23 | 24 | // Hamming is a Hamming windowing function. 25 | // 26 | // Reference: https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows 27 | func Hamming(x float64, n int) float64 { 28 | return hannHamming(0.53836, x, n) 29 | } 30 | 31 | func hannHamming(a, x float64, n int) float64 { 32 | return a - (1-a)*math.Cos(2*x*math.Pi/float64(n)) 33 | } 34 | 35 | // Lanczos is a Lanczos windowing function. 36 | // 37 | // Reference: https://en.wikipedia.org/wiki/Window_function#Lanczos_window 38 | func Lanczos(x float64, n int) float64 { 39 | return Sinc((2 * x / float64(n)) - 1) 40 | } 41 | 42 | // Bartlett is a Bartlett windowing function. 43 | // 44 | // Reference: https://en.wikipedia.org/wiki/Window_function#Triangular_window 45 | func Bartlett(x float64, n int) float64 { 46 | return 1 - 2*math.Abs(x-float64(n)/2)/float64(n) 47 | } 48 | 49 | // Sinc is the cardinal sinc function. Use it to create other window functions. 50 | // 51 | // Reference: https://en.wikipedia.org/wiki/Sinc_function 52 | func Sinc(x float64) float64 { 53 | if x == 0 { 54 | return 1 55 | } 56 | return math.Sin(math.Pi*x) / (math.Pi * x) 57 | } 58 | -------------------------------------------------------------------------------- /window/window_test.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestWindows(t *testing.T) { 10 | for _, v := range []struct { 11 | wf Func 12 | expect []float64 13 | }{ 14 | { 15 | wf: Lanczos, 16 | expect: []float64{ 17 | 3.89817183251937e-17, 18 | 0.23387232094715982, 19 | 0.5045511524271047, 20 | 0.756826728640657, 21 | 0.935489283788639, 22 | 1, 23 | 0.935489283788639, 24 | 0.7568267286406571, 25 | 0.5045511524271046, 26 | 0.23387232094715982, 27 | 3.89817183251937e-17, 28 | }, 29 | }, 30 | { 31 | wf: Hann, 32 | expect: []float64{ 33 | 0, 34 | 0.09549150281252633, 35 | 0.3454915028125263, 36 | 0.6545084971874737, 37 | 0.9045084971874737, 38 | 1, 39 | 0.9045084971874737, 40 | 0.6545084971874737, 41 | 0.3454915028125264, 42 | 0.09549150281252633, 43 | 0, 44 | }, 45 | }, 46 | { 47 | wf: Hamming, 48 | expect: []float64{ 49 | 0.0767199999999999, 50 | 0.16488539471674923, 51 | 0.3957053947167492, 52 | 0.6810146052832506, 53 | 0.9118346052832507, 54 | 1, 55 | 0.9118346052832507, 56 | 0.6810146052832508, 57 | 0.39570539471674926, 58 | 0.16488539471674923, 59 | 0.0767199999999999, 60 | }, 61 | }, 62 | { 63 | wf: Blackman, 64 | expect: []float64{ 65 | -1.3877787807814457e-17, 66 | 0.04021286236252211, 67 | 0.20077014326253045, 68 | 0.5097871376374778, 69 | 0.8492298567374694, 70 | 1, 71 | 0.8492298567374694, 72 | 0.509787137637478, 73 | 0.20077014326253056, 74 | 0.04021286236252208, 75 | -1.3877787807814457e-17, 76 | }, 77 | }, 78 | { 79 | wf: Bartlett, 80 | expect: []float64{ 81 | 0, 82 | 0.19999999999999996, 83 | 0.4, 84 | 0.6, 85 | 0.8, 86 | 1, 87 | 0.8, 88 | 0.6, 89 | 0.4, 90 | 0.19999999999999996, 91 | 0, 92 | }, 93 | }, 94 | } { 95 | n := len(v.expect) 96 | out := make([]float64, n) 97 | for i := 0; i < n; i++ { 98 | out[i] = v.wf(float64(i), n-1) 99 | } 100 | 101 | for i := range out { 102 | if v.expect[i] == 0 { 103 | continue 104 | } 105 | require.InEpsilon(t, v.expect[i], out[i], 1e-9) 106 | } 107 | } 108 | } 109 | --------------------------------------------------------------------------------