├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── lifft.h ├── lifft_dct.h └── test ├── DCT.ipynb ├── FFT.ipynb ├── FreqEst.ipynb ├── MDCT.ipynb ├── Makefile ├── OverlapAdd.ipynb ├── RealFFT.ipynb ├── test.c └── test.cpp /.gitattributes: -------------------------------------------------------------------------------- 1 | test/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.o 3 | test/test 4 | test/.ipynb_checkpoints 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Scott Lembcke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎛️ LiFFT 2 | The little FFT library. 3 | 4 | A simple FFT and DCT2 library in a header only format that makes it easy to drop into a project and use. Performance isn't the main concern, but it's still a bit better than half the speed of [FFTPACK](https://www.netlib.org/fftpack/) or [FFTW](https://fftw.org/). 5 | -------------------------------------------------------------------------------- /lifft.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #ifndef LIFFT_NO_STDLIB 7 | #include 8 | #include 9 | #endif 10 | 11 | #ifndef LIFFT_FLOAT_TYPE 12 | #define LIFFT_FLOAT_TYPE double 13 | #endif 14 | 15 | typedef LIFFT_FLOAT_TYPE lifft_float_t; 16 | #define _LIFFT_PI ((lifft_float_t)3.14159265358979323846) 17 | #define _LIFFT_SQRT_2 ((lifft_float_t)1.4142135623730951) 18 | 19 | #if defined(LIFFT_STD_COMPLEX) 20 | #include 21 | typedef complex LIFFT_FLOAT_TYPE lifft_complex_t; 22 | 23 | static inline lifft_complex_t lifft_complex(lifft_float_t re, lifft_float_t im){return re + im*I;} 24 | static inline lifft_complex_t lifft_cadd(lifft_complex_t x, lifft_complex_t y){return x + y;} 25 | static inline lifft_complex_t lifft_csub(lifft_complex_t x, lifft_complex_t y){return x - y;} 26 | static inline lifft_complex_t lifft_cmul(lifft_complex_t x, lifft_complex_t y){return x*y;} 27 | static inline lifft_complex_t lifft_cdiv(lifft_complex_t x, lifft_complex_t y){return x/y;} 28 | static inline lifft_complex_t lifft_conj(lifft_complex_t x){return conj(x);} 29 | static inline lifft_float_t lifft_cabs(lifft_complex_t x){return cabs(x);} 30 | static inline lifft_float_t lifft_creal(lifft_complex_t x){return creal(x);} 31 | static inline lifft_float_t lifft_cimag(lifft_complex_t x){return cimag(x);} 32 | static inline lifft_complex_t lifft_cispi(lifft_complex_t x){return cexp((_LIFFT_PI*I)*x);} 33 | #elif !defined(LIFFT_COMPLEX_TYPE) 34 | typedef struct {lifft_float_t re, im;} lifft_complex_t; 35 | static inline lifft_complex_t lifft_complex(lifft_float_t re, lifft_float_t im){lifft_complex_t res = {re, im}; return res;} 36 | static inline lifft_complex_t lifft_cadd(lifft_complex_t x, lifft_complex_t y){return lifft_complex(x.re + y.re, x.im + y.im);} 37 | static inline lifft_complex_t lifft_csub(lifft_complex_t x, lifft_complex_t y){return lifft_complex(x.re - y.re, x.im - y.im);} 38 | static inline lifft_complex_t lifft_cmul(lifft_complex_t x, lifft_complex_t y){return lifft_complex(x.re*y.re - x.im*y.im, x.re*y.im + x.im*y.re);} 39 | static inline lifft_complex_t lifft_cdiv(lifft_complex_t x, lifft_complex_t y){return lifft_complex((x.re*y.re + x.im*y.im)/(y.re*y.re + y.im*y.im), (x.im*y.re - x.re*y.im)/(y.re*y.re + y.im*y.im));} 40 | static inline lifft_complex_t lifft_conj(lifft_complex_t x){return lifft_complex(x.re, -x.im);} 41 | static inline lifft_float_t lifft_cabs(lifft_complex_t x){return (lifft_float_t)sqrt(x.re*x.re + x.im*x.im);} 42 | static inline lifft_float_t lifft_creal(lifft_complex_t x){return x.re;} 43 | static inline lifft_float_t lifft_cimag(lifft_complex_t x){return x.im;} 44 | static inline lifft_complex_t lifft_cispi(lifft_float_t x){return lifft_complex((lifft_float_t)cos(_LIFFT_PI*x), (lifft_float_t)sin(_LIFFT_PI*x));} 45 | #endif 46 | 47 | // Compute the forward FFT on complex valued data. 48 | // The length of 'x_in', 'x_out', and 'scratch' must be 'n', which must be a power of two. 49 | void lifft_forward_complex(lifft_complex_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 50 | 51 | // Compute the inverse FFT on complex valued data. 52 | // The length of 'x_in', 'x_out', and 'scratch' must be 'n', which must be a power of two. 53 | void lifft_inverse_complex(lifft_complex_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 54 | 55 | // Compute the forward FFT on re valued data. 56 | // 'x_in' must be length 'n'. 57 | // 'x_out' must be length 'n/2 + 1'. 58 | // 'scratch' must be length 'n/2'. 59 | // 'n' must be a power of two. 60 | void lifft_forward_real(lifft_float_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 61 | 62 | // Compute the inverse FFT on re valued data. 63 | // 'x_in' must be length 'n/2 + 1'. 64 | // 'x_out' must be length 'n'. 65 | // 'scratch' must be length 'n/2'. 66 | // 'n' must be a power of two. 67 | void lifft_inverse_real(lifft_complex_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 68 | 69 | #define LIFFT_APPLY_2D(func, x_in, x_out, n) { \ 70 | typeof(*x_in) _tmp_[n*n]; \ 71 | lifft_complex_t _scratch_[n]; \ 72 | for(int i = 0; i < n; i++) func( x_in + i*n, 1, _tmp_ + i, n, _scratch_, n); \ 73 | for(int i = 0; i < n; i++) func(_tmp_ + i*n, 1, x_out + i, n, _scratch_, n); \ 74 | } 75 | 76 | #ifdef LIFFT_IMPLEMENTATION 77 | 78 | static unsigned _lifft_setup(size_t n, size_t stride_in, size_t stride_out){ 79 | unsigned bits = (unsigned)log2(n); 80 | // Check size. 81 | assert(n == 1u << bits && bits <= 18u); 82 | // Check valid strides. 83 | assert(stride_in && stride_out); 84 | return bits; 85 | } 86 | 87 | // Reverse bits in an integer of up to 18 bits. 88 | static inline size_t _lifft_rev_bits18(size_t n, unsigned bits){ 89 | static const uint8_t REV[] = { 90 | 0x00, 0x20, 0x10, 0x30, 0x08, 0x28, 0x18, 0x38, 0x04, 0x24, 0x14, 0x34, 0x0C, 0x2C, 0x1C, 0x3C, 91 | 0x02, 0x22, 0x12, 0x32, 0x0A, 0x2A, 0x1A, 0x3A, 0x06, 0x26, 0x16, 0x36, 0x0E, 0x2E, 0x1E, 0x3E, 92 | 0x01, 0x21, 0x11, 0x31, 0x09, 0x29, 0x19, 0x39, 0x05, 0x25, 0x15, 0x35, 0x0D, 0x2D, 0x1D, 0x3D, 93 | 0x03, 0x23, 0x13, 0x33, 0x0B, 0x2B, 0x1B, 0x3B, 0x07, 0x27, 0x17, 0x37, 0x0F, 0x2F, 0x1F, 0x3F, 94 | }; 95 | 96 | size_t rev = 0; 97 | rev <<= 6; rev |= REV[n & 0x3F]; n >>= 6; 98 | rev <<= 6; rev |= REV[n & 0x3F]; n >>= 6; 99 | rev <<= 6; rev |= REV[n & 0x3F]; n >>= 6; 100 | return rev >> (18 - bits); 101 | } 102 | 103 | // Iteratively apply passes of Cooley-Tukey. 104 | // 'x' must be shuffled into bit reversed index order, the result will be ordered normally. 105 | static void _lifft_process(lifft_complex_t* x, size_t n){ 106 | size_t stride = 1; 107 | 108 | // Apply a specialized initial pass when n >= 4 109 | if(n >= 4){ 110 | for(size_t i = 0; i < n; i += 4){ 111 | lifft_complex_t s = x[i + 0], t = x[i + 1], p = x[i + 2], q = x[i + 3]; 112 | x[i + 0] = lifft_complex(s.re + t.re + p.re + q.re, s.im + t.im + p.im + q.im); 113 | x[i + 1] = lifft_complex(s.re - t.re + p.im - q.im, s.im - t.im - p.re + q.re); 114 | x[i + 2] = lifft_complex(s.re + t.re - p.re - q.re, s.im + t.im - p.im - q.im); 115 | x[i + 3] = lifft_complex(s.re - t.re - p.im + q.im, s.im - t.im + p.re - q.re); 116 | } 117 | 118 | stride *= 4; 119 | } 120 | 121 | // Iteratively apply radix-2-2 passes. 122 | while(2*stride < n){ 123 | lifft_complex_t wm1 = lifft_cispi(-1.0/(lifft_float_t)stride); 124 | lifft_complex_t wm2 = lifft_cispi(-0.5/(lifft_float_t)stride); 125 | for(size_t i = 0; i < n; i += 4*stride){ 126 | lifft_complex_t w1 = lifft_complex(1, 0); 127 | lifft_complex_t w2 = lifft_complex(1, 0); 128 | for(size_t j = 0; j < stride; j++){ 129 | size_t idx = i + j; 130 | lifft_complex_t p = x[idx + 0*stride], q = lifft_cmul(x[idx + 1*stride], w1); 131 | lifft_complex_t r = x[idx + 2*stride], s = lifft_cmul(x[idx + 3*stride], w1); 132 | w1 = lifft_cmul(w1, wm1); 133 | 134 | lifft_complex_t a = lifft_cadd(p, q); 135 | lifft_complex_t b = lifft_csub(p, q); 136 | lifft_complex_t c = lifft_cmul(lifft_cadd(r, s), w2); 137 | lifft_complex_t d = lifft_cmul(lifft_csub(r, s), lifft_complex(w2.im, -w2.re)); 138 | x[idx + 0*stride] = lifft_cadd(a, c); 139 | x[idx + 1*stride] = lifft_cadd(b, d); 140 | x[idx + 2*stride] = lifft_csub(a, c); 141 | x[idx + 3*stride] = lifft_csub(b, d); 142 | w2 = lifft_cmul(w2, wm2); 143 | } 144 | } 145 | 146 | stride *= 4; 147 | } 148 | 149 | // Apply a final radix-2 pass if needed. 150 | if(stride < n){ 151 | lifft_complex_t wm = lifft_cispi(-1/(lifft_float_t)stride); 152 | for(size_t i = 0; i < n; i += 2*stride){ 153 | lifft_complex_t w = lifft_complex(1, 0); 154 | for(size_t j = 0; j < stride; j++){ 155 | lifft_complex_t p = x[i + j + 0*stride], q = lifft_cmul(w, x[i + j + 1*stride]); 156 | x[i + j + 0*stride] = lifft_cadd(p, q); 157 | x[i + j + 1*stride] = lifft_csub(p, q); 158 | w = lifft_cmul(w, wm); 159 | } 160 | } 161 | } 162 | } 163 | 164 | void lifft_forward_complex(lifft_complex_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 165 | unsigned bits = _lifft_setup(n, stride_in, stride_out); 166 | 167 | // Copy to scratch[] in shuffled order, apply the FFT, then copy to the output. 168 | for(size_t i = 0; i < n; i++) scratch[_lifft_rev_bits18(i, bits)] = x_in[i*stride_in]; 169 | _lifft_process(scratch, n); 170 | if(scratch != x_out) for(size_t i = 0; i < n; i++) x_out[i*stride_out] = scratch[i]; 171 | } 172 | 173 | void lifft_inverse_complex(lifft_complex_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 174 | unsigned bits = _lifft_setup(n, stride_in, stride_out); 175 | 176 | // Compute iFFT via iFFT(x) = FFT(reverse(x/n)) 177 | lifft_complex_t coef = lifft_complex((lifft_float_t)1.0/n, 0); 178 | for(size_t i = 0; i < n; i++) scratch[_lifft_rev_bits18(-i & (n - 1), bits)] = lifft_cmul(x_in[i*stride_in], coef); 179 | _lifft_process(scratch, n); 180 | if(scratch != x_out) for(size_t i = 0; i < n; i++) x_out[i*stride_out] = scratch[i]; 181 | } 182 | 183 | void lifft_forward_real(lifft_float_t x_in[], size_t stride_in, lifft_complex_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 184 | unsigned bits = _lifft_setup(n, stride_in, stride_out) - 1; 185 | 186 | // Copy as [evens + odds*im] 187 | for(size_t i = 0; i < n/2; i++) scratch[_lifft_rev_bits18(i, bits)] = lifft_complex(x_in[(2*i + 0)*stride_in]/2, x_in[(2*i + 1)*stride_in]/2); 188 | _lifft_process(scratch, n/2); 189 | 190 | lifft_complex_t w = lifft_complex(0, -1), wm = lifft_cispi((lifft_float_t)-2.0/n); 191 | for(size_t i = 0; i <= n/4; i++){ 192 | // Unpack using even/odd fft symmetry 193 | lifft_complex_t p = scratch[i], q = lifft_conj(scratch[-i&(n/2 - 1)]); 194 | lifft_complex_t xe = lifft_cadd(p, q), xo = lifft_cmul(lifft_csub(p, q), w); 195 | w = lifft_cmul(w, wm); 196 | 197 | // Apply final stage of Cooley Tukey 198 | x_out[i*stride_out] = lifft_cadd(xe, xo); 199 | x_out[(n/2 - i)*stride_out] = lifft_conj(lifft_csub(xe, xo)); 200 | } 201 | } 202 | 203 | void lifft_inverse_real(lifft_complex_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 204 | unsigned bits = _lifft_setup(n, stride_in, stride_out) - 1; 205 | 206 | lifft_complex_t w = lifft_complex(0, 1), wm = lifft_cispi((lifft_float_t)2.0/n); 207 | for(size_t i = 0; i <= n/4; i++){ 208 | // Calculate evens/odds from re fft symmetry 209 | lifft_complex_t p = x_in[i*stride_in], q = lifft_conj(x_in[(n/2 - i)*stride_in]); 210 | lifft_complex_t xe = lifft_cadd(p, q), xo = lifft_cmul(lifft_csub(p, q), w); 211 | w = lifft_cmul(w, wm); 212 | 213 | // Pack using even/odd symetry 214 | scratch[_lifft_rev_bits18(i, bits)] = lifft_conj(lifft_cadd(xe, xo)); 215 | scratch[_lifft_rev_bits18(-i & (n/2 - 1), bits)] = lifft_csub(xe, xo); 216 | } 217 | 218 | _lifft_process(scratch, n/2); 219 | 220 | // Extract evens from re and odd from im 221 | for(size_t i = 0; i < n/2; i++){ 222 | x_out[(2*i + 0)*stride_out] = +lifft_creal(scratch[i])/n; 223 | x_out[(2*i + 1)*stride_out] = -lifft_cimag(scratch[i])/n; 224 | } 225 | } 226 | 227 | #endif 228 | -------------------------------------------------------------------------------- /lifft_dct.h: -------------------------------------------------------------------------------- 1 | // Compute the forward DCT II. 2 | // 'x_in' and 'x_out' must be length 'n'. 3 | // 'scratch' must be length 'n/2'. 4 | // 'n' must be a power of two. 5 | void lifft_forward_dct(lifft_float_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 6 | 7 | // Compute the inverse DCT II (DCT III). 8 | // 'x_in' and 'x_out' must be length 'n'. 9 | // 'scratch' must be length 'n/2 + 1'. 10 | // 'n' must be a power of two. 11 | void lifft_inverse_dct(lifft_float_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n); 12 | 13 | #ifdef LIFFT_IMPLEMENTATION 14 | 15 | void lifft_forward_dct(lifft_float_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 16 | unsigned bits = _lifft_setup(n, stride_in, stride_out) - 1; 17 | 18 | // To calculate the DCT II, you need to double and mirror x_in, BUT! 19 | // That means evens and odds are mirrored, so you can compute the other via symmetry. 20 | for(size_t i = 0; i < n/4; i++){ 21 | scratch[_lifft_rev_bits18(i, bits)] = lifft_complex(x_in[(4*i + 0)*stride_in], x_in[(4*i + 2)*stride_in]); 22 | scratch[_lifft_rev_bits18(n/2 - i - 1, bits)] = lifft_complex(x_in[(4*i + 3)*stride_in], x_in[(4*i + 1)*stride_in]); 23 | } 24 | 25 | // Compute real valued FFT 26 | _lifft_process(scratch, n/2); 27 | lifft_complex_t w0 = lifft_complex(0, -1), wm0 = lifft_cispi((lifft_float_t)-2.0/n); 28 | for(size_t i = 0; i <= n/4; i++){ 29 | // Unpack using even/odd fft symmetry 30 | lifft_complex_t p = scratch[i], q = lifft_conj(scratch[-i&(n/2 - 1)]); 31 | lifft_complex_t xe = lifft_cadd(p, q), xo = lifft_cmul(lifft_csub(p, q), w0); 32 | w0 = lifft_cmul(w0, wm0); 33 | 34 | // Apply final stage of Cooley Tukey 35 | scratch[i] = lifft_cadd(xe, xo); 36 | scratch[(n/2 - i)] = lifft_conj(lifft_csub(xe, xo)); 37 | } 38 | 39 | // TODO can these loops be fused? 40 | // Compute the DCT II using the even/odd symmetry. 41 | lifft_complex_t w1 = lifft_complex(1, 0), wm1 = lifft_cispi((lifft_float_t)-0.5/n); 42 | for(size_t i = 0; i <= n/2; i++){ 43 | lifft_complex_t p = lifft_cmul(scratch[i], w1); 44 | w1 = lifft_cmul(w1, wm1); 45 | 46 | x_out[(-i&(n - 1))*stride_out] = -lifft_cimag(p); 47 | x_out[i*stride_out] = lifft_creal(p); 48 | } 49 | } 50 | 51 | // TODO this is quite the mess. See if I can compute this from the DCT III directly instead. 52 | void lifft_inverse_dct(lifft_float_t x_in[], size_t stride_in, lifft_float_t x_out[], size_t stride_out, lifft_complex_t scratch[], size_t n){ 53 | unsigned bits = _lifft_setup(n, stride_in, stride_out) - 1; 54 | 55 | lifft_complex_t wm = lifft_cispi((lifft_float_t)0.5/n), w = lifft_complex(0.5, 0); 56 | scratch[0] = lifft_cmul(lifft_complex(x_in[0], 0), w); 57 | w = lifft_cmul(w, wm); 58 | 59 | for(size_t i = 1; i <= n/2; i++){ 60 | scratch[i] = lifft_cmul(lifft_complex(x_in[i*stride_in], -x_in[(n - i)*stride_in]), w); 61 | w = lifft_cmul(w, wm); 62 | } 63 | 64 | // TODO why does this need so many temporary array. Aurghrgrh! 65 | lifft_float_t scratch_real[n]; 66 | lifft_complex_t scratch2[n]; 67 | lifft_inverse_real(scratch, 1, scratch_real, 1, scratch2, n); 68 | 69 | for(size_t i = 0; i < n/4; i++){ 70 | x_out[(4*i + 0)*stride_out] = scratch_real[2*i + 0]; 71 | x_out[(4*i + 1)*stride_out] = scratch_real[n - 2*i - 1]; 72 | x_out[(4*i + 2)*stride_out] = scratch_real[2*i + 1]; 73 | x_out[(4*i + 3)*stride_out] = scratch_real[n - 2*i - 2]; 74 | } 75 | } 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /test/DCT.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "fdab8b90-0b12-4a14-bdd5-3543739ea9c5", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "using FFTW\n", 11 | "using Plots" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "14ee33e6-e0e9-463b-a261-9196a8c6805e", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "flip_idx(i, n) = -(i - 1) & (n - 1) + 1\n", 22 | "lin_phase(x, θ) = x.*cispi.((0:length(x) - 1).*(θ/N))\n", 23 | "\n", 24 | "x = [681, 683, 414, 987, 336, 583, 121, 772]\n", 25 | "X = fft(x)\n", 26 | "x2 = [x; reverse(x)]\n", 27 | "X2 = fft(x2)\n", 28 | "N = length(x)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "cd9e1b1c-17e3-4d1a-aacc-3bc3119b0d66", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# Calculate the whole thing using a -half sample rotation instead of 0's\n", 39 | "round.(lin_phase(X2, -0.5))" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "id": "3e41a62a-f6d4-44bf-b71b-d256e5e7ecbb", 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "# Compute only the even elements and use conjugate symmetry for the other half\n", 50 | "Xe = fft(x2[1:2:2N])\n", 51 | "Xo = conj(lin_phase(Xe, -1))\n", 52 | "round.(lin_phase(Xe + Xo, -0.5))" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "029c9805-3345-4ecf-b5d8-fdeca711b3bd", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "# Calculate above using just real valued even fft coefficients\n", 63 | "Xe = rfft(x2[1:2:2N])\n", 64 | "X = zeros(Complex, N)\n", 65 | "foo = zeros(Complex, N÷2 +1)\n", 66 | "w = 1\n", 67 | "for i in 1:N÷2 + 1\n", 68 | " foo[i] = p = 2*Xe[i]*w\n", 69 | " w *= cispi(-0.5/N)\n", 70 | " \n", 71 | " X[flip_idx(i, N)] = -imag(p)\n", 72 | " X[i] = real(p)\n", 73 | "end\n", 74 | "\n", 75 | "round.([Xe foo/2])" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "id": "5bfb68aa-1df4-4954-8c50-d51e75c51276", 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [] 85 | } 86 | ], 87 | "metadata": { 88 | "kernelspec": { 89 | "display_name": "Julia 1.8.2", 90 | "language": "julia", 91 | "name": "julia-1.8" 92 | }, 93 | "language_info": { 94 | "file_extension": ".jl", 95 | "mimetype": "application/julia", 96 | "name": "julia", 97 | "version": "1.8.2" 98 | } 99 | }, 100 | "nbformat": 4, 101 | "nbformat_minor": 5 102 | } 103 | -------------------------------------------------------------------------------- /test/FFT.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import math, random\n", 10 | "import numpy as np\n", 11 | "import scipy.fftpack as fft" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# x = np.array([random.random() + 0j for _ in range(16)])\n", 21 | "x = np.array([\n", 22 | "\t0.70203658, 0.30785784, 0.80697642, 0.2063156 ,\n", 23 | "\t0.74611309, 0.44949445, 0.58790534, 0.94034123,\n", 24 | "\t0.86815133, 0.78308922, 0.51704855, 0.58557402,\n", 25 | "\t0.49798021, 0.43429341, 0.52435585, 0.47455634,\n", 26 | "])\n", 27 | "N, n = len(x), len(x)//2\n", 28 | "\n", 29 | "x, fft.dct(x)" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# Single real FFT\n", 39 | "xe, xo = x[0::2], x[1::2]\n", 40 | "x2 = xe + xo*1j\n", 41 | "Z = fft.fft(x2)\n", 42 | "\n", 43 | "Xe = np.zeros(n, complex)\n", 44 | "Xo = np.zeros(n, complex)\n", 45 | "for i in range(n):\n", 46 | "\tXe[i] = (Z[i] + Z[-i].conj())*(0.5 - 0.0j)\n", 47 | "\tXo[i] = (Z[i] - Z[-i].conj())*(0.0 - 0.5j)\n", 48 | "\n", 49 | "X = np.zeros(N, complex)\n", 50 | "w, wm = 1, math.e**(-2j*math.pi/N)\n", 51 | "X[n] = Xe[0] - Xo[0]\n", 52 | "for i in range(n):\n", 53 | "\tXi = Xe[i] + Xo[i]*w\n", 54 | "\tX[i], X[-i] = Xi, Xi.conj()\n", 55 | "\tw *= wm\n", 56 | "\n", 57 | "\n", 58 | "np.round(X - fft.fft(x), 4)\n", 59 | "# Z, X, Xe, Xo" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "# Single real iFFT (TODO not implemented)\n", 69 | "X = fft.fft(x)\n", 70 | "\n", 71 | "Xe = np.zeros(n, complex)\n", 72 | "Xo = np.zeros(n, complex)\n", 73 | "for i in range(n):\n", 74 | "\tXe[i] = (X[i] + X[-i].conj())/2\n", 75 | "\tXo[i] = (X[i] - X[-i].conj())/2*math.e**(2j*math.pi*i/N)\n", 76 | "\n", 77 | "Z = Xe + Xo*1j\n", 78 | "# Z - fft.fft(x)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "# DCT via FFT\n", 88 | "xdct = np.zeros(2*N, complex)\n", 89 | "xdct[ 0: N: 1] = x\n", 90 | "xdct[-1:-N-1:-1] = x\n", 91 | "\n", 92 | "Xdct = fft.fft(xdct)[:N]\n", 93 | "\n", 94 | "# Apply half sample rotation.\n", 95 | "w, wm = 1, math.e**(-0.5j*math.pi/N)\n", 96 | "for i in range(N):\n", 97 | "\tXdct[i] *= w\n", 98 | "\tw *= wm\n", 99 | "\n", 100 | "np.round(Xdct - fft.dct(x), 10)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "# iDCT via iFFT\n", 110 | "Xdct = fft.dct(x) + 0j\n", 111 | "\n", 112 | "Xtmp = np.zeros(2*N, complex)\n", 113 | "\n", 114 | "# Apply half sample rotation.\n", 115 | "w, wm = 1, math.e**(0.5j*math.pi/N)\n", 116 | "for i in range(N):\n", 117 | "\tX = Xdct[i]*w\n", 118 | "\tXtmp[i] = X\n", 119 | "\tXtmp[-i] = X.conj()\n", 120 | "\tw *= wm\n", 121 | "\n", 122 | "np.round(x - fft.ifft(Xtmp)[:N], 10)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "# DCT via real FFT\n", 132 | "xdct = np.zeros(2*N, complex)\n", 133 | "xdct[ 0: N: 1] = x\n", 134 | "xdct[-1:-N-1:-1] = x\n", 135 | "\n", 136 | "xe, xo = xdct[0::2], xdct[1::2]\n", 137 | "xdct = xe + xo*1j\n", 138 | "Z = fft.fft(xdct)\n", 139 | "\n", 140 | "Xe = np.zeros(N, complex)\n", 141 | "Xo = np.zeros(N, complex)\n", 142 | "Xdct = np.zeros(N, complex)\n", 143 | "w, wm = 1, math.e**(-0.5j*math.pi/N)\n", 144 | "for i in range(N):\n", 145 | "\tXe[i] = (Z[i] + Z[-i].conj())*(0.5 - 0.0j)\n", 146 | "\tXo[i] = (Z[i] - Z[-i].conj())*(0.0 - 0.5j)\n", 147 | "\tXdct[i] = (Xe[i] + Xo[i]*w*w)*w\n", 148 | "\tw *= wm\n", 149 | "\n", 150 | "np.round(Xdct - fft.dct(x), 10)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "# iDCT via real iFFT\n", 160 | "Xdct = fft.dct(x) + 0j\n", 161 | "\n", 162 | "# Apply half sample rotation.\n", 163 | "w, wm = 1, math.e**(0.5j*math.pi/N)\n", 164 | "for i in range(N):\n", 165 | "\tXdct[i] *= w\n", 166 | "\t# print(w)\n", 167 | "\tw *= wm\n", 168 | "\n", 169 | "Z = np.zeros(N, complex)\n", 170 | "w, wm = 1, math.e**(1j*math.pi/N)\n", 171 | "for i in range(N):\n", 172 | "\tX0, X1 = Xdct[i], Xdct[-i]\n", 173 | "\tXe = (X0 + X1.conj())\n", 174 | "\tXo = (X0 - X1.conj())*w\n", 175 | "\tZ[i] = Xe + Xo*1j\n", 176 | "\tw *= wm\n", 177 | "Z[0] *= complex(0.5, 0.5)\n", 178 | "\n", 179 | "z = fft.fft(Z.conj()).conj()*0.5/N\n", 180 | "np.round(xdct - z, 10)" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "x = np.array([random.random() + 0j for _ in range(1 << 16)])\n", 190 | "for _ in range(4000):\n", 191 | "\tx = fft.fft(x)\n", 192 | "\tx = fft.ifft(x)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": null, 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [] 201 | } 202 | ], 203 | "metadata": { 204 | "interpreter": { 205 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" 206 | }, 207 | "kernelspec": { 208 | "display_name": "Python 3 (ipykernel)", 209 | "language": "python", 210 | "name": "python3" 211 | }, 212 | "language_info": { 213 | "codemirror_mode": { 214 | "name": "ipython", 215 | "version": 3 216 | }, 217 | "file_extension": ".py", 218 | "mimetype": "text/x-python", 219 | "name": "python", 220 | "nbconvert_exporter": "python", 221 | "pygments_lexer": "ipython3", 222 | "version": "3.11.0" 223 | } 224 | }, 225 | "nbformat": 4, 226 | "nbformat_minor": 4 227 | } 228 | -------------------------------------------------------------------------------- /test/FreqEst.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "05675329-4e3e-40b1-8ebd-5a8098846498", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "using Plots\n", 11 | "using FFTW\n", 12 | "using DSP" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "9a6fa349-4a41-468f-97cd-d42f5847a508", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "fs = 8\n", 23 | "N = 128\n", 24 | "len = 2*N\n", 25 | "\n", 26 | "f = 0.132\n", 27 | "x = cos.((0:len - 1)*(f*2π/fs))\n", 28 | "\n", 29 | "plot(plot(x), plot(abs.(rfft(x))))" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "id": "af1972c4-9ac3-4e4e-b3d6-131e6c719603", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "idxs, step = 1:N, 1\n", 40 | "w = triang(N)\n", 41 | "X0 = rfft(w.*x[idxs .+ 4*step])\n", 42 | "X1 = rfft(w.*x[idxs .+ 5*step])\n", 43 | "\n", 44 | "overlap_factor = N/step\n", 45 | "expect = cis(-2π/overlap_factor)\n", 46 | "println(\"N:$N, step:$step, overlap:$(overlap_factor)x\")\n", 47 | "println(\"fstep:$(fs/N), expect:$(round(expect; digits=2))\")\n", 48 | "\n", 49 | "for i in 0:8\n", 50 | " idx = Int(i + 1)\n", 51 | " Δθ = angle(X1[idx]*conj(X0[idx])*(expect^i))\n", 52 | " fi = (fs/N)*(i + Δθ*overlap_factor/2π)\n", 53 | " mag = round(abs(X0[idx]))\n", 54 | " println(\"$(i): $(round(fi; digits=4))s mag:$(round(mag))\")\n", 55 | "end" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "bf2afa7d-bcb1-467d-872a-e957832c1c50", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "plot(w)\n", 66 | "plot!(w.*x[idxs .+ 16])" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "id": "e80f65ac-8f4a-4782-bded-9c9842180d98", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [] 76 | } 77 | ], 78 | "metadata": { 79 | "kernelspec": { 80 | "display_name": "Julia 1.8.1", 81 | "language": "julia", 82 | "name": "julia-1.8" 83 | }, 84 | "language_info": { 85 | "file_extension": ".jl", 86 | "mimetype": "application/julia", 87 | "name": "julia", 88 | "version": "1.8.2" 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 5 93 | } 94 | -------------------------------------------------------------------------------- /test/MDCT.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "id": "coordinate-tennessee", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import math, sys\n", 11 | "import numpy as np\n", 12 | "import numpy.linalg as la\n", 13 | "import matplotlib.pyplot as pp\n", 14 | "from PIL import Image\n", 15 | "import scipy.fft as fft\n", 16 | "import zlib\n", 17 | "\n", 18 | "rand = np.random.rand" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "fb9b6feb-eb1d-4830-a5ea-1e6b7f58d955", 24 | "metadata": {}, 25 | "source": [ 26 | "https://en.wikipedia.org/wiki/Modified_discrete_cosine_transform" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 5, 32 | "id": "d7cb95f7-5c77-4d4a-bbc4-8dd757538cce", 33 | "metadata": {}, 34 | "outputs": [ 35 | { 36 | "data": { 37 | "text/plain": [ 38 | "array([0.632, 0.138, 0.863, 0.587, 0.284, 0.502, 0.856, 0.672])" 39 | ] 40 | }, 41 | "execution_count": 5, 42 | "metadata": {}, 43 | "output_type": "execute_result" 44 | } 45 | ], 46 | "source": [ 47 | "N = 8\n", 48 | "A = np.zeros(N, float)\n", 49 | "B = rand(N)\n", 50 | "np.round(B, 3)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 6, 56 | "id": "cc2369c7-5eb6-4687-8b13-551818ecec24", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "def mdct(A, B):\n", 61 | " n = len(A)//2\n", 62 | " x = np.zeros(2*n, float)\n", 63 | " for i in range(0, n):\n", 64 | " x[n - i - 1] = -B[i] - B[2*n - i - 1]\n", 65 | " x[n + i + 0] = +A[i] - A[2*n - i - 1]\n", 66 | " return fft.dct(x, 4)\n", 67 | "\n", 68 | "def imdct(z):\n", 69 | " n = len(z)//2\n", 70 | " x = fft.dct(z, 4)\n", 71 | " A = np.zeros(2*n, float)\n", 72 | " B = np.zeros(2*n, float)\n", 73 | " for i in range(0, n):\n", 74 | " A[0*n + i] = +x[1*n + i + 0]/(8*n)\n", 75 | " A[1*n + i] = -x[2*n - i - 1]/(8*n)\n", 76 | " B[0*n + i] = -x[1*n - i - 1]/(8*n)\n", 77 | " B[1*n + i] = -x[0*n + i + 0]/(8*n)\n", 78 | " return A, B" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 7, 84 | "id": "2563d887-d766-4932-8597-5d299420a4b8", 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "data": { 89 | "text/plain": [ 90 | "(array([0.632, 0.138, 0.863, 0.587, 0.284, 0.502, 0.856, 0.672]),\n", 91 | " array([0.632, 0.138, 0.863, 0.587, 0.284, 0.502, 0.856, 0.672]))" 92 | ] 93 | }, 94 | "execution_count": 7, 95 | "metadata": {}, 96 | "output_type": "execute_result" 97 | } 98 | ], 99 | "source": [ 100 | "np.round(imdct(mdct(A, B))[1] + imdct(mdct(B, A))[0], 3), np.round(B, 3)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 8, 106 | "id": "f215607d-f697-4a8a-9066-328070805f44", 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/plain": [ 112 | "[]" 113 | ] 114 | }, 115 | "execution_count": 8, 116 | "metadata": {}, 117 | "output_type": "execute_result" 118 | }, 119 | { 120 | "data": { 121 | "image/png": "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\n", 122 | "text/plain": [ 123 | "
" 124 | ] 125 | }, 126 | "metadata": {}, 127 | "output_type": "display_data" 128 | } 129 | ], 130 | "source": [ 131 | "N = 1024\n", 132 | "I = complex(0, 1)\n", 133 | "z = np.sqrt(rand(N//2 + 1))*np.exp(I*rand(N//2 + 1)*math.pi)\n", 134 | "z *= 1.4**-np.arange(0, N//2 + 1)\n", 135 | "x = 7000*fft.irfft(z)\n", 136 | "pp.plot(x)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 11, 142 | "id": "ab7d626b-9fa1-42a1-8f21-a4c311ef84c2", 143 | "metadata": {}, 144 | "outputs": [ 145 | { 146 | "data": { 147 | "text/plain": [ 148 | "array([0.1 , 0.09801987, 0.09607894, 0.09417645, 0.09231163,\n", 149 | " 0.09048374, 0.08869204, 0.08693582, 0.08521438, 0.08352702,\n", 150 | " 0.08187308, 0.08025188, 0.07866279, 0.07710516, 0.07557837,\n", 151 | " 0.07408182, 0.0726149 , 0.07117703, 0.06976763, 0.06838614,\n", 152 | " 0.067032 , 0.06570468, 0.06440364, 0.06312836, 0.06187834,\n", 153 | " 0.06065307, 0.05945205, 0.05827483, 0.05712091, 0.05598984,\n", 154 | " 0.05488116, 0.05379444, 0.05272924, 0.05168513, 0.0506617 ,\n", 155 | " 0.04965853, 0.04867523, 0.04771139, 0.04676664, 0.0458406 ,\n", 156 | " 0.0449329 , 0.04404317, 0.04317105, 0.04231621, 0.04147829,\n", 157 | " 0.04065697, 0.0398519 , 0.03906278, 0.03828929, 0.03753111,\n", 158 | " 0.03678794, 0.03605949, 0.03534547, 0.03464558, 0.03395955,\n", 159 | " 0.03328711, 0.03262798, 0.0319819 , 0.03134862, 0.03072787,\n", 160 | " 0.03011942, 0.02952302, 0.02893842, 0.0283654 ])" 161 | ] 162 | }, 163 | "execution_count": 11, 164 | "metadata": {}, 165 | "output_type": "execute_result" 166 | }, 167 | { 168 | "data": { 169 | "image/png": "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MK2IiIgELRUqIiIiErRUqAQBp9PJj370I5xOp9VR+j1dS9/RtfQdXUvf0bX0nf5yLfv9YFoREREJXbqjIiIiIkFLhYqIiIgELRUqIiIiErRUqIiIiEjQUqESBB566CEKCwuJiopi+vTprF271upIQWXx4sVMnTqV+Ph4MjIyuPLKK9m1a9cxx3R0dHDHHXeQmppKXFwc11xzDVVVVcccU1paysKFC4mJiSEjI4PvfOc7uFyuQH4pQeeBBx7AMAzuvPPOns/pWvbeoUOH+PznP09qairR0dGMHTuW9evX97xvmib33Xcf2dnZREdHM3fuXPbs2XNMG3V1dSxatIiEhASSkpK49dZbaWlpCfSXYim3280Pf/hDBg4cSHR0NIMHD+ZnP/vZMXvA6Fqe2IoVK7jsssvIycnBMAxeeumlY9731XXbvHkz5513HlFRUeTl5fGrX/3K31/aMV+EWOiZZ54xIyMjzccee8zctm2b+aUvfclMSkoyq6qqrI4WNObNm2f+7W9/M7du3WoWFRWZl156qZmfn2+2tLT0HHPbbbeZeXl55tKlS83169eb55xzjnnuuef2vO9yucwxY8aYc+fONTdu3Gi+/vrrZlpamnnvvfda8SUFhbVr15qFhYXmuHHjzG9+85s9n9e17J26ujqzoKDAvOmmm8w1a9aYxcXF5ptvvmnu3bu355gHHnjATExMNF966SVz06ZN5uWXX24OHDjQbG9v7zlm/vz55vjx483Vq1eb7733njlkyBDz+uuvt+JLssz9999vpqammq+99pq5f/9+87nnnjPj4uLM3//+9z3H6Fqe2Ouvv25+//vfN1944QUTMF988cVj3vfFdWtsbDQzMzPNRYsWmVu3bjWffvppMzo62nzkkUcC8jWqULHYtGnTzDvuuKPnY7fbbebk5JiLFy+2MFVwq66uNgFz+fLlpmmaZkNDgxkREWE+99xzPcfs2LHDBMxVq1aZpun9ZrbZbGZlZWXPMX/+85/NhIQEs7OzM7BfQBBobm42hw4dai5ZssScPXt2T6Gia9l73/3ud81Zs2ad9H2Px2NmZWWZv/71r3s+19DQYDqdTvPpp582TdM0t2/fbgLmunXreo7573//axqGYR46dMh/4YPMwoULzVtuueWYz1199dXmokWLTNPUteytTxYqvrpuf/rTn8zk5ORjvr+/+93vmsOHD/fzV+SlRz8W6urqYsOGDcydO7fnczabjblz57Jq1SoLkwW3xsZGAFJSUgDYsGED3d3dx1zHESNGkJ+f33MdV61axdixY8nMzOw5Zt68eTQ1NbFt27YApg8Od9xxBwsXLjzmmoGu5Zl45ZVXmDJlCtdddx0ZGRlMnDiRRx99tOf9/fv3U1lZecy1TExMZPr06cdcy6SkJKZMmdJzzNy5c7HZbKxZsyZwX4zFzj33XJYuXcru3bsB2LRpEytXrmTBggWArmVf+eq6rVq1ivPPP5/IyMieY+bNm8euXbuor6/3+9fR7zcl7M9qampwu93H/MAHyMzMZOfOnRalCm4ej4c777yTmTNnMmbMGAAqKyuJjIwkKSnpmGMzMzOprKzsOeZE1/noe+HkmWee4cMPP2TdunXHvadr2XvFxcX8+c9/5u677+Z//ud/WLduHd/4xjeIjIzkxhtv7LkWJ7pWH7+WGRkZx7zvcDhISUkJq2v5ve99j6amJkaMGIHdbsftdnP//fezaNEiAF3LPvLVdausrGTgwIHHtXH0veTkZL/k78nj19ZFfOyOO+5g69atrFy50uoo/VJZWRnf/OY3WbJkCVFRUVbH6dc8Hg9TpkzhF7/4BQATJ05k69atPPzww9x4440Wp+tfnn32WZ566in++c9/Mnr0aIqKirjzzjvJycnRtRTN+rFSWloadrv9uBkVVVVVZGVlWZQqeH3ta1/jtdde49133yU3N7fn81lZWXR1ddHQ0HDM8R+/jllZWSe8zkffCxcbNmygurqaSZMm4XA4cDgcLF++nD/84Q84HA4yMzN1LXspOzubUaNGHfO5kSNHUlpaCnx0LU71/Z2VlUV1dfUx77tcLurq6sLqWn7nO9/he9/7Hp/97GcZO3YsN9xwA3fddReLFy8GdC37ylfXzerveRUqFoqMjGTy5MksXbq053Mej4elS5cyY8YMC5MFF9M0+drXvsaLL77IO++8c9wtyMmTJxMREXHMddy1axelpaU913HGjBls2bLlmG/IJUuWkJCQcNwvm1B20UUXsWXLFoqKinpeU6ZMYdGiRT1/1rXsnZkzZx43TX737t0UFBQAMHDgQLKyso65lk1NTaxZs+aYa9nQ0MCGDRt6jnnnnXfweDxMnz49AF9FcGhra8NmO/bXkd1ux+PxALqWfeWr6zZjxgxWrFhBd3d3zzFLlixh+PDhfn/sA2h6stWeeeYZ0+l0mo8//ri5fft288tf/rKZlJR0zIyKcHf77bebiYmJ5rJly8yKioqeV1tbW88xt912m5mfn2++88475vr1680ZM2aYM2bM6Hn/6JTaSy65xCwqKjLfeOMNMz09Peym1J7Ix2f9mKauZW+tXbvWdDgc5v3332/u2bPHfOqpp8yYmBjzySef7DnmgQceMJOSksyXX37Z3Lx5s3nFFVeccGroxIkTzTVr1pgrV640hw4dGvJTaj/pxhtvNAcMGNAzPfmFF14w09LSzHvuuafnGF3LE2tubjY3btxobty40QTMBx980Ny4caNZUlJimqZvrltDQ4OZmZlp3nDDDebWrVvNZ555xoyJidH05HDyxz/+0czPzzcjIyPNadOmmatXr7Y6UlABTvj629/+1nNMe3u7+dWvftVMTk42Y2JizKuuusqsqKg4pp0DBw6YCxYsMKOjo820tDTzW9/6ltnd3R3gryb4fLJQ0bXsvVdffdUcM2aM6XQ6zREjRph/+ctfjnnf4/GYP/zhD83MzEzT6XSaF110kblr165jjqmtrTWvv/56My4uzkxISDBvvvlms7m5OZBfhuWamprMb37zm2Z+fr4ZFRVlDho0yPz+979/zHRYXcsTe/fdd0/48/HGG280TdN3123Tpk3mrFmzTKfTaQ4YMMB84IEHAvUlmoZpfmzpPxEREZEgojEqIiIiErRUqIiIiEjQUqEiIiIiQUuFioiIiAQtFSoiIiIStFSoiIiISNBSoSIiIiJBS4WKiIiIBC0VKiIiIhK0VKiIiIhI0FKhIiIiIkFLhYqIiIgErf8PSC9KkksPvf8AAAAASUVORK5CYII=\n", 170 | "text/plain": [ 171 | "
" 172 | ] 173 | }, 174 | "metadata": {}, 175 | "output_type": "display_data" 176 | } 177 | ], 178 | "source": [ 179 | "BLOCK_SIZE = 64\n", 180 | "cursor = 0\n", 181 | "out = np.zeros(N, float)\n", 182 | "dct = np.zeros(BLOCK_SIZE, float)\n", 183 | "prev_in = np.zeros(BLOCK_SIZE, float)\n", 184 | "prev_out = np.zeros(BLOCK_SIZE, float)\n", 185 | "\n", 186 | "window = np.arange(0, 2*BLOCK_SIZE) + 0.5\n", 187 | "# sin() window:\n", 188 | "window = math.sqrt(2)*np.sin(window*(math.pi/(2*BLOCK_SIZE)))\n", 189 | "# Vorbis window:\n", 190 | "# window = np.sin(window*(math.pi/(2*BLOCK_SIZE)))**2\n", 191 | "# window = math.sqrt(2)*np.sin(math.pi/2*window)\n", 192 | "w0 = window[:BLOCK_SIZE]\n", 193 | "w1 = window[BLOCK_SIZE:]\n", 194 | "# w0, w1 = 1, 1\n", 195 | "\n", 196 | "q = 0.1*np.exp(-0.02*np.arange(0, BLOCK_SIZE))\n", 197 | "\n", 198 | "while cursor < len(x):\n", 199 | " block_in = x[cursor:cursor + BLOCK_SIZE]\n", 200 | " z = mdct(prev_in*w0, block_in*w1)\n", 201 | " # z = fft.dct(block_in)\n", 202 | " z = np.round(z*q)\n", 203 | " dct = np.maximum(dct, abs(z))\n", 204 | " prev_in = block_in\n", 205 | " \n", 206 | " z /= q\n", 207 | " a, b = imdct(z)\n", 208 | " a *= w0; b *= w1\n", 209 | " block, prev_out = prev_out + a, b\n", 210 | " # block = fft.idct(z)\n", 211 | " \n", 212 | " out[cursor:cursor + BLOCK_SIZE] = block\n", 213 | " cursor += BLOCK_SIZE\n", 214 | "\n", 215 | "pp.plot(x)\n", 216 | "pp.plot(np.arange(N) - 1*BLOCK_SIZE, out)\n", 217 | "q" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "id": "7b68dcb8-dc66-46c3-998e-b1ccf4f3df19", 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "pp.ylim((0, 10))\n", 228 | "pp.plot(np.log2(dct))" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "id": "7ad80e4b-d6d9-4ca2-882a-52dbd5da20ea", 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [] 238 | } 239 | ], 240 | "metadata": { 241 | "kernelspec": { 242 | "display_name": "Python 3 (ipykernel)", 243 | "language": "python", 244 | "name": "python3" 245 | }, 246 | "language_info": { 247 | "codemirror_mode": { 248 | "name": "ipython", 249 | "version": 3 250 | }, 251 | "file_extension": ".py", 252 | "mimetype": "text/x-python", 253 | "name": "python", 254 | "nbconvert_exporter": "python", 255 | "pygments_lexer": "ipython3", 256 | "version": "3.11.0" 257 | } 258 | }, 259 | "nbformat": 4, 260 | "nbformat_minor": 5 261 | } 262 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -g -O0 -Wno-format 2 | CFLAGS += -O3 3 | LDLIBS = -lm 4 | 5 | default: test 6 | 7 | clean: 8 | -rm *.o test 9 | 10 | test.o: ../lifft.h ../lifft_dct.h 11 | test: test.o 12 | -------------------------------------------------------------------------------- /test/OverlapAdd.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "711ff4d2-147c-48c0-ba80-f28536f2d292", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "using Plots\n", 11 | "using DSP\n", 12 | "using FFTW" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "7971177e-d4bc-408f-a5d7-e788ba8d9703", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "Nx = 256\n", 23 | "x = rand(Nx)\n", 24 | "# x = sin.(8*π/N*(1:Nx))\n", 25 | "\n", 26 | "X = rfft(x)\n", 27 | "X[1] = 0\n", 28 | "X .*= exp.(-4e-2*(1:Nx/2+1))\n", 29 | "x = irfft(X, Nx)\n", 30 | "plot(x)" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "560f6929-0320-4e6e-b561-b4df5e38600e", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "Nw = 32\n", 41 | "# y = 1 .- range(-1, 1, Nw).^2\n", 42 | "# y = sinc.(range(-4, 4, Nw))\n", 43 | "\n", 44 | "r = range(-2, 2, Nw)\n", 45 | "y = imag.(exp.(-0.5.*r.^2).*(cispi.(r) .+ 0.5))\n", 46 | "\n", 47 | "y *= 1.0/sum(abs.(y))\n", 48 | "plot(y)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "dcf62980-9fa4-4bee-92a9-3a20291e6135", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "i = 1:Nx\n", 59 | "plot(i, x)\n", 60 | "\n", 61 | "Y = rfft([y; zeros(Nx - Nw)])\n", 62 | "plot!(i .- Nw/2, irfft(rfft(x).*Y, Nx))" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "32eec269-3947-403f-9f20-838f449f3689", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "Y = rfft([y; zeros(Nw)])\n", 73 | "prev = zeros(Nw)\n", 74 | "z = zeros(Nx)\n", 75 | "\n", 76 | "for i in 1:Nw:Nx\n", 77 | " idxs = i:i + Nw - 1\n", 78 | " X = rfft([x[idxs]; zeros(Nw)])\n", 79 | " foo = irfft(X.*Y, 2*Nw)\n", 80 | " z[idxs] .= prev .+ foo[1:Nw]\n", 81 | " prev = foo[Nw + 1:2*Nw]\n", 82 | "end\n", 83 | "\n", 84 | "i = 1:Nx\n", 85 | "plot(i, x)\n", 86 | "plot!(i .- Nw/2, z)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "id": "5bd011e2-4193-4b52-84f4-35ba82fa396b", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "plot(w)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "f11c66a1-6c48-490f-84a5-3ec259de819c", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [] 106 | } 107 | ], 108 | "metadata": { 109 | "kernelspec": { 110 | "display_name": "Julia 1.8.0", 111 | "language": "julia", 112 | "name": "julia-1.8" 113 | }, 114 | "language_info": { 115 | "file_extension": ".jl", 116 | "mimetype": "application/julia", 117 | "name": "julia", 118 | "version": "1.8.0" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 5 123 | } 124 | -------------------------------------------------------------------------------- /test/RealFFT.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "30e91480-cc7c-4e13-864e-8408b14c46f1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "using Plots\n", 11 | "using FFTW" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "a4b45c7b-4daa-48ab-8cc0-3f65452af7c2", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "# Reflect index as -i&(n - 1), but for 1-based indexes\n", 22 | "flip_idx(i, n) = -(i - 1)&(n - 1) + 1\n", 23 | "\n", 24 | "x = [681, 683, 414, 987, 336, 583, 121, 772]\n", 25 | "y = [354, 929, 058, 303, 314, 552, 248, 280]\n", 26 | "N = length(x)\n", 27 | "\n", 28 | "round.([fft(x) fft(y)])[1:N÷2 + 1, :]" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "bca8434f-bb4a-4b00-8ec9-3e2c4465e164", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# Calculate simultaneous real FFTs\n", 39 | "n = N÷2 + 1\n", 40 | "X, Y = zeros(Complex, n), zeros(Complex, n)\n", 41 | "\n", 42 | "# Combine x and y, then calculate it's FFT\n", 43 | "XY = fft(x + y*im)/2\n", 44 | "\n", 45 | "# Now separate the two using even/odd symmetry property\n", 46 | "for i in 1:n\n", 47 | " s = XY[i]\n", 48 | " t = conj(XY[flip_idx(i, N)])\n", 49 | " X[i] = (s + t)\n", 50 | " Y[i] = (s - t)*im\n", 51 | "end\n", 52 | "\n", 53 | "round.([X Y])" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "id": "22fe44cc-1c82-4a01-8eeb-49d2da8b604b", 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# Calculate real valued FFT\n", 64 | "X = zeros(Complex, N÷2 + 1)\n", 65 | "\n", 66 | "# Calculate FFTs of evens and odds simultaneously\n", 67 | "xe, xo = x[1:2:N], x[2:2:N]\n", 68 | "Xeo = fft(xe + xo*im)/2\n", 69 | "\n", 70 | "w, wm = -im, cispi(-2/N)\n", 71 | "for i in 1:N÷4 + 1\n", 72 | " p = Xeo[i]\n", 73 | " q = conj(Xeo[flip_idx(i, N÷2)])\n", 74 | " Xei = (p + q)\n", 75 | " Xowi = (p - q)*w\n", 76 | " X[i] = Xei + Xowi\n", 77 | " X[N÷2 - i + 2] = conj(Xei - Xowi)\n", 78 | " w *= wm\n", 79 | "end\n", 80 | "\n", 81 | "round.([X rfft(x)])" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "id": "f6423fc8-f71b-4fd8-bed3-d106c32b09fa", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "# Calculate real valued ifft\n", 92 | "X = rfft(x)\n", 93 | "Xeo = zeros(ComplexF64, N÷2)\n", 94 | "\n", 95 | "w, wm = im, cispi(2/N)\n", 96 | "for i in 1:N÷4 + 1\n", 97 | " p = X[i]\n", 98 | " q = conj(X[N÷2 - i + 2])\n", 99 | " Xei = (p + q)\n", 100 | " Xoi = (p - q)*w\n", 101 | " Xeo[i] = conj(Xei + Xoi)\n", 102 | " Xeo[flip_idx(i, N÷2)] = (Xei - Xoi)\n", 103 | " w *= wm\n", 104 | "end\n", 105 | "xeo = fft(Xeo)/N\n", 106 | "[x reshape(transpose([real(xeo) -imag(xeo)]), N)]" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "id": "e5e9f22f-6c27-4d17-8d33-2b7a26c9a0f4", 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [] 116 | } 117 | ], 118 | "metadata": { 119 | "kernelspec": { 120 | "display_name": "Julia 1.8.2", 121 | "language": "julia", 122 | "name": "julia-1.8" 123 | }, 124 | "language_info": { 125 | "file_extension": ".jl", 126 | "mimetype": "application/julia", 127 | "name": "julia", 128 | "version": "1.8.2" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 5 133 | } 134 | -------------------------------------------------------------------------------- /test/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | // #define LIFFT_FLOAT_TYPE float 7 | // #define LIFFT_STD_COMPLEX 8 | #define LIFFT_IMPLEMENTATION 9 | #include "../lifft.h" 10 | #include "../lifft_dct.h" 11 | 12 | void fft_it(size_t n, int iterations){ 13 | lifft_complex_t x0[n], X[n], x1[n]; 14 | for(unsigned i = 0; i < n; i++){ 15 | x0[i] = lifft_complex((float)rand()/(float)RAND_MAX, 0); 16 | x1[i] = lifft_complex(0, 0); 17 | } 18 | 19 | for(unsigned i = 0; i < iterations; i++){ 20 | lifft_complex_t scratch[n]; 21 | lifft_forward_complex(x0, 1, X, 1, scratch, n); 22 | lifft_inverse_complex(X, 1, x1, 1, scratch, n); 23 | 24 | // lifft_complex_t scratch[n/2]; 25 | // lifft_forward_real(x0, 2, X, 1, scratch, n); 26 | // lifft_inverse_real(X, 1, x1, 2, scratch, n); 27 | } 28 | 29 | double err = 0; 30 | for(unsigned i = 0; i < n; i++) err += lifft_cabs(lifft_csub(x0[i], x1[i])); 31 | printf("err on FFT size %9d: %e\n", (int)n, err); 32 | } 33 | 34 | void fft_it_2d(size_t n, int iterations){ 35 | lifft_complex_t x0[n*n], X[n*n], x1[n*n]; 36 | for(unsigned i = 0; i < n*n; i++) x0[i] = lifft_complex((float)rand()/(float)RAND_MAX, 0); 37 | 38 | for(unsigned i = 0; i < iterations; i++){ 39 | LIFFT_APPLY_2D(lifft_forward_complex, x0, X, n); 40 | LIFFT_APPLY_2D(lifft_inverse_complex, X, x1, n); 41 | } 42 | 43 | double err = 0; 44 | for(unsigned i = 0; i < n*n; i++) err += lifft_cabs(lifft_csub(x0[i], x1[i])); 45 | printf("err on FFT size %3d x %3d: %e\n", (int)n, (int)n, err); 46 | } 47 | 48 | void dct_it(size_t n, int iterations){ 49 | lifft_float_t x0[n], X[n], x1[n]; 50 | for(unsigned i = 0; i < n; i++) x0[i] = (float)rand()/(float)RAND_MAX; 51 | 52 | for(unsigned i = 0; i < iterations; i++){ 53 | lifft_complex_t scratch[n/2 + 1]; 54 | lifft_forward_dct(x0, 1, X, 1, scratch, n); 55 | lifft_inverse_dct(X, 1, x1, 1, scratch, n); 56 | } 57 | 58 | double err = 0; 59 | for(unsigned i = 0; i < n; i++) err += fabs(x0[i] - x1[i]); 60 | printf("err on DCT size %9d: %e\n", (int)n, err); 61 | } 62 | 63 | void dct_it_2d(size_t n, int iterations){ 64 | lifft_float_t x0[n*n], X[n*n], x1[n*n]; 65 | for(unsigned i = 0; i < n*n; i++) x0[i] = (float)rand()/(float)RAND_MAX; 66 | 67 | for(unsigned i = 0; i < iterations; i++){ 68 | LIFFT_APPLY_2D(lifft_forward_dct, x0, X, n); 69 | LIFFT_APPLY_2D(lifft_inverse_dct, X, x1, n); 70 | } 71 | 72 | double err = 0; 73 | for(unsigned i = 0; i < n*n; i++) err += fabs(x0[i] - x1[i]); 74 | printf("err on DCT size %3d x %3d: %e\n", (int)n, (int)n, err); 75 | } 76 | 77 | int main(int argc, const char* argv[]){ 78 | int iterations = 100; 79 | 80 | fft_it(32, iterations); 81 | fft_it(1 << 16, iterations); 82 | fft_it_2d(8, iterations); 83 | fft_it_2d(1 << 8, iterations); 84 | 85 | dct_it(32, iterations); 86 | dct_it(1 << 16, iterations); 87 | dct_it_2d(32, iterations); 88 | dct_it_2d(1 << 8, iterations); 89 | 90 | // unsigned n = 8; 91 | // lifft_complex_t x0[n], X[n], x1[n]; 92 | // for(unsigned i = 0; i < n; i++) x0[i] = lifft_complex((float)rand()/(float)RAND_MAX, 0); 93 | 94 | // lifft_forward_real(x0, 2, X, 1, n); 95 | // lifft_inverse_real(X, 1, x1, 2, n); 96 | 97 | // lifft_forward_dct((lifft_float_t*)x0, 2, (lifft_float_t*)X, 2, n); 98 | // lifft_inverse_dct((lifft_float_t*)X, 2, (lifft_float_t*)x1, 2, n); 99 | 100 | // for(unsigned i = 0; i < n; i++){ 101 | // printf("% 2d: %.3f %.3f -> %+.3f%+.3f -> %+.3f%+.3f\n", i, x0[i], X[i], x1[i]); 102 | // } 103 | 104 | return EXIT_SUCCESS; 105 | } 106 | -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | #define LIFFT_IMPLEMENTATION 2 | #include "../lifft.h" 3 | --------------------------------------------------------------------------------