From cb33149c173ebe8863d511aeba6c7ea69db544de Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Wed, 19 Feb 2020 17:15:49 +0100 Subject: [PATCH 01/17] Remove deprecated functions, templatize, Speedup --- Examples/FFT_01/FFT_01.ino | 19 +- Examples/FFT_02/FFT_02.ino | 21 +- Examples/FFT_03/FFT_03.ino | 17 +- Examples/FFT_04/FFT_04.ino | 18 +- Examples/FFT_05/FFT_05.ino | 19 +- Examples/FFT_speedup/FFT_speedup.ino | 129 +++++++ README.md | 98 +++--- changeLog.txt | 6 + keywords.txt | 44 +-- library.json | 7 +- library.properties | 2 +- src/arduinoFFT.cpp | 446 ------------------------- src/arduinoFFT.h | 481 ++++++++++++++++++++++----- 13 files changed, 673 insertions(+), 634 deletions(-) create mode 100644 Examples/FFT_speedup/FFT_speedup.ino delete mode 100644 src/arduinoFFT.cpp diff --git a/Examples/FFT_01/FFT_01.ino b/Examples/FFT_01/FFT_01.ino index caefe80..22b5024 100644 --- a/Examples/FFT_01/FFT_01.ino +++ b/Examples/FFT_01/FFT_01.ino @@ -1,7 +1,9 @@ /* Example of use of the FFT libray - Copyright (C) 2014 Enrique Condes + + Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,7 +32,6 @@ #include "arduinoFFT.h" -arduinoFFT FFT = arduinoFFT(); /* Create FFT object */ /* These values can be changed in order to evaluate the functions */ @@ -38,6 +39,7 @@ const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 const double signalFrequency = 1000; const double samplingFrequency = 5000; const uint8_t amplitude = 100; + /* These are the input and output vectors Input vectors receive computed results from FFT @@ -45,6 +47,9 @@ Input vectors receive computed results from FFT double vReal[samples]; double vImag[samples]; +/* Create FFT object */ +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency); + #define SCL_INDEX 0x00 #define SCL_TIME 0x01 #define SCL_FREQUENCY 0x02 @@ -62,25 +67,25 @@ void loop() double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (twoPi * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ + vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } /* Print the results of the simulated sampling according to time */ Serial.println("Data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */ + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ Serial.println("Weighed data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */ + FFT.compute(FFTDirection::Forward); /* Compute FFT */ Serial.println("Computed Real values:"); PrintVector(vReal, samples, SCL_INDEX); Serial.println("Computed Imaginary values:"); PrintVector(vImag, samples, SCL_INDEX); - FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */ + FFT.complexToMagnitude(); /* Compute magnitudes */ Serial.println("Computed magnitudes:"); PrintVector(vReal, (samples >> 1), SCL_FREQUENCY); - double x = FFT.MajorPeak(vReal, samples, samplingFrequency); + double x = FFT.majorPeak(); Serial.println(x, 6); while(1); /* Run Once */ // delay(2000); /* Repeat after delay */ diff --git a/Examples/FFT_02/FFT_02.ino b/Examples/FFT_02/FFT_02.ino index cb97e29..bfa1804 100644 --- a/Examples/FFT_02/FFT_02.ino +++ b/Examples/FFT_02/FFT_02.ino @@ -4,7 +4,9 @@ The exponent is calculated once before the excecution since it is a constant. This saves resources during the excecution of the sketch and reduces the compiled size. The sketch shows the time that the computing is taking. - Copyright (C) 2014 Enrique Condes + + Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,15 +25,12 @@ #include "arduinoFFT.h" -arduinoFFT FFT = arduinoFFT(); /* Create FFT object */ /* These values can be changed in order to evaluate the functions */ - const uint16_t samples = 64; const double sampling = 40; const uint8_t amplitude = 4; -uint8_t exponent; const double startFrequency = 2; const double stopFrequency = 16.4; const double step_size = 0.1; @@ -43,6 +42,9 @@ Input vectors receive computed results from FFT double vReal[samples]; double vImag[samples]; +/* Create FFT object */ +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, sampling); + unsigned long time; #define SCL_INDEX 0x00 @@ -54,7 +56,6 @@ void setup() { Serial.begin(115200); Serial.println("Ready"); - exponent = FFT.Exponent(samples); } void loop() @@ -67,24 +68,24 @@ void loop() double cycles = (((samples-1) * frequency) / sampling); for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (twoPi * cycles)) / samples))) / 2.0); + vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0); vImag[i] = 0; //Reset the imaginary values vector for each new frequency } /*Serial.println("Data:"); PrintVector(vReal, samples, SCL_TIME);*/ time=millis(); - FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */ + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ /*Serial.println("Weighed data:"); PrintVector(vReal, samples, SCL_TIME);*/ - FFT.Compute(vReal, vImag, samples, exponent, FFT_FORWARD); /* Compute FFT */ + FFT.compute(FFTDirection::Forward); /* Compute FFT */ /*Serial.println("Computed Real values:"); PrintVector(vReal, samples, SCL_INDEX); Serial.println("Computed Imaginary values:"); PrintVector(vImag, samples, SCL_INDEX);*/ - FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */ + FFT.complexToMagnitude(); /* Compute magnitudes */ /*Serial.println("Computed magnitudes:"); PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);*/ - double x = FFT.MajorPeak(vReal, samples, sampling); + double x = FFT.majorPeak(); Serial.print(frequency); Serial.print(": \t\t"); Serial.print(x, 4); diff --git a/Examples/FFT_03/FFT_03.ino b/Examples/FFT_03/FFT_03.ino index 9e1640c..2e50613 100644 --- a/Examples/FFT_03/FFT_03.ino +++ b/Examples/FFT_03/FFT_03.ino @@ -1,7 +1,9 @@ /* Example of use of the FFT libray to compute FFT for a signal sampled through the ADC. - Copyright (C) 2018 Enrique Condés and Ragnar Ranøyen Homb + + Copyright (C) 2018 Enrique Condés and Ragnar Ranøyen Homb + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,14 +22,12 @@ #include "arduinoFFT.h" -arduinoFFT FFT = arduinoFFT(); /* Create FFT object */ /* These values can be changed in order to evaluate the functions */ #define CHANNEL A0 const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 const double samplingFrequency = 100; //Hz, must be less than 10000 due to ADC - unsigned int sampling_period_us; unsigned long microseconds; @@ -38,6 +38,9 @@ Input vectors receive computed results from FFT double vReal[samples]; double vImag[samples]; +/* Create FFT object */ +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency); + #define SCL_INDEX 0x00 #define SCL_TIME 0x01 #define SCL_FREQUENCY 0x02 @@ -66,18 +69,18 @@ void loop() /* Print the results of the sampling according to time */ Serial.println("Data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */ + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ Serial.println("Weighed data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */ + FFT.compute(FFTDirection::Forward); /* Compute FFT */ Serial.println("Computed Real values:"); PrintVector(vReal, samples, SCL_INDEX); Serial.println("Computed Imaginary values:"); PrintVector(vImag, samples, SCL_INDEX); - FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */ + FFT.complexToMagnitude(); /* Compute magnitudes */ Serial.println("Computed magnitudes:"); PrintVector(vReal, (samples >> 1), SCL_FREQUENCY); - double x = FFT.MajorPeak(vReal, samples, samplingFrequency); + double x = FFT.majorPeak(); Serial.println(x, 6); //Print out what frequency is the most dominant. while(1); /* Run Once */ // delay(2000); /* Repeat after delay */ diff --git a/Examples/FFT_04/FFT_04.ino b/Examples/FFT_04/FFT_04.ino index 5c1fcd5..b125991 100644 --- a/Examples/FFT_04/FFT_04.ino +++ b/Examples/FFT_04/FFT_04.ino @@ -1,7 +1,9 @@ /* Example of use of the FFT libray - Copyright (C) 2018 Enrique Condes + + Copyright (C) 2018 Enrique Condes + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -31,7 +33,6 @@ #include "arduinoFFT.h" -arduinoFFT FFT = arduinoFFT(); /* Create FFT object */ /* These values can be changed in order to evaluate the functions */ @@ -39,6 +40,7 @@ const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 const double signalFrequency = 1000; const double samplingFrequency = 5000; const uint8_t amplitude = 100; + /* These are the input and output vectors Input vectors receive computed results from FFT @@ -46,6 +48,8 @@ Input vectors receive computed results from FFT double vReal[samples]; double vImag[samples]; +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency); + #define SCL_INDEX 0x00 #define SCL_TIME 0x01 #define SCL_FREQUENCY 0x02 @@ -62,15 +66,15 @@ void loop() double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (twoPi * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ + vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } - FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */ - FFT.Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */ - FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */ + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ + FFT.compute(FFTDirection::Forward); /* Compute FFT */ + FFT.complexToMagnitude(); /* Compute magnitudes */ PrintVector(vReal, samples>>1, SCL_PLOT); - double x = FFT.MajorPeak(vReal, samples, samplingFrequency); + double x = FFT.majorPeak(); while(1); /* Run Once */ // delay(2000); /* Repeat after delay */ } diff --git a/Examples/FFT_05/FFT_05.ino b/Examples/FFT_05/FFT_05.ino index 7abe3b6..a6f4df7 100644 --- a/Examples/FFT_05/FFT_05.ino +++ b/Examples/FFT_05/FFT_05.ino @@ -1,7 +1,9 @@ /* Example of use of the FFT libray - Copyright (C) 2014 Enrique Condes + + Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -31,7 +33,6 @@ #include "arduinoFFT.h" -arduinoFFT FFT = arduinoFFT(); /* Create FFT object */ /* These values can be changed in order to evaluate the functions */ @@ -39,6 +40,7 @@ const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 const double signalFrequency = 1000; const double samplingFrequency = 5000; const uint8_t amplitude = 100; + /* These are the input and output vectors Input vectors receive computed results from FFT @@ -46,6 +48,9 @@ Input vectors receive computed results from FFT double vReal[samples]; double vImag[samples]; +/* Create FFT object */ +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency); + #define SCL_INDEX 0x00 #define SCL_TIME 0x01 #define SCL_FREQUENCY 0x02 @@ -63,27 +68,27 @@ void loop() double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (twoPi * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ + vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } /* Print the results of the simulated sampling according to time */ Serial.println("Data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */ + FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ Serial.println("Weighed data:"); PrintVector(vReal, samples, SCL_TIME); - FFT.Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */ + FFT.compute(FFTDirection::Forward); /* Compute FFT */ Serial.println("Computed Real values:"); PrintVector(vReal, samples, SCL_INDEX); Serial.println("Computed Imaginary values:"); PrintVector(vImag, samples, SCL_INDEX); - FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */ + FFT.complexToMagnitude(); /* Compute magnitudes */ Serial.println("Computed magnitudes:"); PrintVector(vReal, (samples >> 1), SCL_FREQUENCY); double x; double v; - FFT.MajorPeak(vReal, samples, samplingFrequency, &x, &v); + FFT.majorPeak(x, v); Serial.print(x, 6); Serial.print(", "); Serial.println(v, 6); diff --git a/Examples/FFT_speedup/FFT_speedup.ino b/Examples/FFT_speedup/FFT_speedup.ino new file mode 100644 index 0000000..a059a17 --- /dev/null +++ b/Examples/FFT_speedup/FFT_speedup.ino @@ -0,0 +1,129 @@ +/* + + Example of use of the FFT libray to compute FFT for a signal sampled through the ADC + with speedup through different arduinoFFT options. Based on examples/FFT_03/FFT_03.ino + + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +*/ + +// There are two speedup options for some of the FFT code: + +// Define this to use reciprocal multiplication for division and some more speedups that might decrease precision +//#define FFT_SPEED_OVER_PRECISION + +// Define this to use a low-precision square root approximation instead of the regular sqrt() call +// This might only work for specific use cases, but is significantly faster. Only works for ArduinoFFT. +//#define FFT_SQRT_APPROXIMATION + +#include "arduinoFFT.h" + +/* +These values can be changed in order to evaluate the functions +*/ +#define CHANNEL A0 +const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 +const float samplingFrequency = 100; //Hz, must be less than 10000 due to ADC +unsigned int sampling_period_us; +unsigned long microseconds; + +/* +These are the input and output vectors +Input vectors receive computed results from FFT +*/ +float vReal[samples]; +float vImag[samples]; + +/* +Allocate space for FFT window weighing factors, so they are calculated only the first time windowing() is called. +If you don't do this, a lot of calculations are necessary, depending on the window function. +*/ +float weighingFactors[samples]; + +/* Create FFT object with weighing factor storage */ +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency, weighingFactors); + +#define SCL_INDEX 0x00 +#define SCL_TIME 0x01 +#define SCL_FREQUENCY 0x02 +#define SCL_PLOT 0x03 + +void setup() +{ + sampling_period_us = round(1000000*(1.0/samplingFrequency)); + Serial.begin(115200); + Serial.println("Ready"); +} + +void loop() +{ + /*SAMPLING*/ + microseconds = micros(); + for(int i=0; i> 1), SCL_FREQUENCY); + float x = FFT.majorPeak(); + Serial.println(x, 6); //Print out what frequency is the most dominant. + while(1); /* Run Once */ + // delay(2000); /* Repeat after delay */ +} + +void PrintVector(float *vData, uint16_t bufferSize, uint8_t scaleType) +{ + for (uint16_t i = 0; i < bufferSize; i++) + { + float abscissa; + /* Print abscissa value */ + switch (scaleType) + { + case SCL_INDEX: + abscissa = (i * 1.0); + break; + case SCL_TIME: + abscissa = ((i * 1.0) / samplingFrequency); + break; + case SCL_FREQUENCY: + abscissa = ((i * 1.0 * samplingFrequency) / samples); + break; + } + Serial.print(abscissa, 6); + if(scaleType==SCL_FREQUENCY) + Serial.print("Hz"); + Serial.print(" "); + Serial.println(vData[i], 4); + } + Serial.println(); +} diff --git a/README.md b/README.md index 37861ba..feb4d7a 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,28 @@ arduinoFFT ========== -Fast Fourier Transform for Arduino +# Fast Fourier Transform for Arduino This is a fork from https://code.google.com/p/makefurt/ which has been abandoned since 2011. +~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. +Tested on Arduino 1.6.11 and 1.8.10. -This is a C++ library for Arduino for computing FFT. Now it works both on Arduino and C projects. - -Tested on Arduino 1.6.11 - -### Installation on Arduino +## Installation on Arduino Use the Arduino Library Manager to install and keep it updated. Just look for arduinoFFT. Only for Arduino 1.5+ -### Manual installation on Arduino +## Manual installation on Arduino -To install this library, just place this entire folder as a subfolder in your Arduino installation +To install this library, just place this entire folder as a subfolder in your Arduino installation. When installed, this library should look like: -When installed, this library should look like: +`Arduino\libraries\arduinoFTT` (this library's folder) +`Arduino\libraries\arduinoFTT\arduinoFTT.h` (the library header file, uses 32 bit floats or 64bit doubles) +`Arduino\libraries\arduinoFTT\keywords.txt` (the syntax coloring file) +`Arduino\libraries\arduinoFTT\examples` (the examples in the "open" menu) +`Arduino\libraries\arduinoFTT\LICENSE` (GPL license file) +`Arduino\libraries\arduinoFTT\README.md` (this file) -Arduino\libraries\arduinoFTT (this library's folder) -Arduino\libraries\arduinoFTT\arduinoFTT.cpp (the library implementation file, uses 32 bits floats vectors) -Arduino\libraries\arduinoFTT\arduinoFTT.h (the library header file, uses 32 bits floats vectors) -Arduino\libraries\arduinoFTT\keywords.txt (the syntax coloring file) -Arduino\libraries\arduinoFTT\examples (the examples in the "open" menu) -Arduino\libraries\arduinoFTT\readme.md (this file) - -### Building on Arduino +## Building on Arduino After this library is installed, you just have to start the Arduino application. You may see a few warning messages as it's built. @@ -36,46 +32,50 @@ select arduinoFTT. This will add a corresponding line to the top of your sketch `#include ` -### TODO +## TODO * Ratio table for windowing function. * Document windowing functions advantages and disadvantages. * Optimize usage and arguments. * Add new windowing functions. -* Spectrum table? +* ~~Spectrum table?~~ -### API +## API -* **arduinoFFT**(void); -* **arduinoFFT**(double *vReal, double *vImag, uint16_t samples, double samplingFrequency); -Constructor -* **~arduinoFFT**(void); -Destructor -* **ComplexToMagnitude**(double *vReal, double *vImag, uint16_t samples); -* **ComplexToMagnitude**(); -* **Compute**(double *vReal, double *vImag, uint16_t samples, uint8_t dir); -* **Compute**(double *vReal, double *vImag, uint16_t samples, uint8_t power, uint8_t dir); -* **Compute**(uint8_t dir); +* **ArduinoFFT**(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * weighingFactors = nullptr); +Constructor. +The type `T` can be `float` or `double`. `vReal` and `vImag` are pointers to arrays of real and imaginary data and have to be allocated outside of ArduinoFFT. `samples` is the number of samples in `vReal` and `vImag` and `weighingFactors` (if specified). `samplingFrequency` is the sample frequency of the data. `weighingFactors` can optionally be specified to cache weighing factors for the windowing function. This speeds up repeated calls to **windowing()** significantly. + +* **~ArduinoFFT**(void); +Destructor. +* **complexToMagnitude**(); +Convert complex values to their magnitude and store in vReal. +* **compute**(FFTDirection dir); Calcuates the Fast Fourier Transform. -* **DCRemoval**(double *vData, uint16_t samples); -* **DCRemoval**(); +* **dcRemoval**(); Removes the DC component from the sample data. -* **MajorPeak**(double *vD, uint16_t samples, double samplingFrequency); -* **MajorPeak**(); +* **majorPeak**(); Looks for and returns the frequency of the biggest spike in the analyzed signal. -* **Revision**(void); +* **revision**(); Returns the library revision. -* **Windowing**(double *vData, uint16_t samples, uint8_t windowType, uint8_t dir); -* **Windowing**(uint8_t windowType, uint8_t dir); +* **windowing**(FFTWindow windowType, FFTDirection dir); Performs a windowing function on the values array. The possible windowing options are: - * FFT_WIN_TYP_RECTANGLE - * FFT_WIN_TYP_HAMMING - * FFT_WIN_TYP_HANN - * FFT_WIN_TYP_TRIANGLE - * FFT_WIN_TYP_NUTTALL - * FFT_WIN_TYP_BLACKMAN - * FFT_WIN_TYP_BLACKMAN_NUTTALL - * FFT_WIN_TYP_BLACKMAN_HARRIS - * FFT_WIN_TYP_FLT_TOP - * FFT_WIN_TYP_WELCH -* **Exponent**(uint16_t value); -Calculates and returns the base 2 logarithm of the given value. + * Rectangle + * Hamming + * Hann + * Triangle + * Nuttall + * Blackman + * Blackman_Nuttall + * Blackman_Harris + * Flat_top + * Welch + +## Special flags + +You can define these before including arduinoFFT.h: + +* #define FFT_SPEED_OVER_PRECISION +Define this to use reciprocal multiplication for division and some more speedups that might decrease precision. + +* #define FFT_SQRT_APPROXIMATION +Define this to use a low-precision square root approximation instead of the regular sqrt() call. This might only work for specific use cases, but is significantly faster. Only works if `T == float`. diff --git a/changeLog.txt b/changeLog.txt index e2cb0fb..b6dc9ad 100644 --- a/changeLog.txt +++ b/changeLog.txt @@ -1,3 +1,9 @@ +02/19/20 v1.9.0 +Remove deprecated API. Consistent renaming of functions to lowercase. +Make template to be able to use float or double type (float brings a ~70% speed increase on ESP32). +Add option to provide cache for window function weighing factors (~50% speed increase on ESP32). +Add some #defines to enable math approximisations to further speed up code (~50% speed increase on ESP32). + 01/27/20 v1.5.5 Lookup table for constants c1 and c2 used during FFT comupting. This increases the FFT computing speed in around 5%. diff --git a/keywords.txt b/keywords.txt index 1daabde..c0f1804 100644 --- a/keywords.txt +++ b/keywords.txt @@ -6,35 +6,35 @@ # Datatypes (KEYWORD1) ####################################### -arduinoFFT KEYWORD1 +ArduinoFFT KEYWORD1 +FFTDirection KEYWORD1 +FFTWindow KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) ####################################### -ComplexToMagnitude KEYWORD2 -Compute KEYWORD2 -DCRemoval KEYWORD2 -Windowing KEYWORD2 -Exponent KEYWORD2 -Revision KEYWORD2 -MajorPeak KEYWORD2 +complexToMagnitude KEYWORD2 +compute KEYWORD2 +dcRemoval KEYWORD2 +windowing KEYWORD2 +exponent KEYWORD2 +revision KEYWORD2 +majorPeak KEYWORD2 ####################################### # Constants (LITERAL1) ####################################### -twoPi LITERAL1 -fourPi LITERAL1 -FFT_FORWARD LITERAL1 -FFT_REVERSE LITERAL1 -FFT_WIN_TYP_RECTANGLE LITERAL1 -FFT_WIN_TYP_HAMMING LITERAL1 -FFT_WIN_TYP_HANN LITERAL1 -FFT_WIN_TYP_TRIANGLE LITERAL1 -FFT_WIN_TYP_NUTTALL LITERAL1 -FFT_WIN_TYP_BLACKMAN LITERAL1 -FFT_WIN_TYP_BLACKMAN_NUTTALL LITERAL1 -FFT_WIN_TYP_BLACKMAN_HARRIS LITERAL1 -FFT_WIN_TYP_FLT_TOP LITERAL1 -FFT_WIN_TYP_WELCH LITERAL1 +Forward LITERAL1 +Reverse LITERAL1 +Rectangle LITERAL1 +Hamming LITERAL1 +Hann LITERAL1 +Triangle LITERAL1 +Nuttall LITERAL1 +Blackman LITERAL1 +Blackman_Nuttall LITERAL1 +Blackman_Harris LITERAL1 +Flat_top LITERAL1 +Welch LITERAL1 diff --git a/library.json b/library.json index 32c7606..e3b8371 100644 --- a/library.json +++ b/library.json @@ -18,9 +18,14 @@ "name": "Didier Longueville", "url": "http://www.arduinoos.com/", "email": "contact@arduinoos.com" + }, + { + "name": "Bim Overbohm", + "url": "https://github.com/HorstBaerbel", + "email": "bim.overbohm@googlemail.com" } ], - "version": "1.5.5", + "version": "1.9.0", "frameworks": ["arduino","mbed","espidf"], "platforms": "*" } diff --git a/library.properties b/library.properties index cd86480..986fbe3 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=arduinoFFT -version=1.5.5 +version=1.9.0 author=Enrique Condes maintainer=Enrique Condes sentence=A library for implementing floating point Fast Fourier Transform calculations on Arduino. diff --git a/src/arduinoFFT.cpp b/src/arduinoFFT.cpp deleted file mode 100644 index f552fc9..0000000 --- a/src/arduinoFFT.cpp +++ /dev/null @@ -1,446 +0,0 @@ -/* - - FFT libray - Copyright (C) 2010 Didier Longueville - Copyright (C) 2014 Enrique Condes - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -*/ - -#include "arduinoFFT.h" - -arduinoFFT::arduinoFFT(void) -{ // Constructor - #warning("This method is deprecated and may be removed on future revisions.") -} - -arduinoFFT::arduinoFFT(double *vReal, double *vImag, uint16_t samples, double samplingFrequency) -{// Constructor - this->_vReal = vReal; - this->_vImag = vImag; - this->_samples = samples; - this->_samplingFrequency = samplingFrequency; - this->_power = Exponent(samples); -} - -arduinoFFT::~arduinoFFT(void) -{ -// Destructor -} - -uint8_t arduinoFFT::Revision(void) -{ - return(FFT_LIB_REV); -} - -void arduinoFFT::Compute(double *vReal, double *vImag, uint16_t samples, uint8_t dir) -{ - #warning("This method is deprecated and may be removed on future revisions.") - Compute(vReal, vImag, samples, Exponent(samples), dir); -} - -void arduinoFFT::Compute(uint8_t dir) -{// Computes in-place complex-to-complex FFT / - // Reverse bits / - uint16_t j = 0; - for (uint16_t i = 0; i < (this->_samples - 1); i++) { - if (i < j) { - Swap(&this->_vReal[i], &this->_vReal[j]); - if(dir==FFT_REVERSE) - Swap(&this->_vImag[i], &this->_vImag[j]); - } - uint16_t k = (this->_samples >> 1); - while (k <= j) { - j -= k; - k >>= 1; - } - j += k; - } - // Compute the FFT / -#ifdef __AVR__ - uint8_t index = 0; -#endif - double c1 = -1.0; - double c2 = 0.0; - uint16_t l2 = 1; - for (uint8_t l = 0; (l < this->_power); l++) { - uint16_t l1 = l2; - l2 <<= 1; - double u1 = 1.0; - double u2 = 0.0; - for (j = 0; j < l1; j++) { - for (uint16_t i = j; i < this->_samples; i += l2) { - uint16_t i1 = i + l1; - double t1 = u1 * this->_vReal[i1] - u2 * this->_vImag[i1]; - double t2 = u1 * this->_vImag[i1] + u2 * this->_vReal[i1]; - this->_vReal[i1] = this->_vReal[i] - t1; - this->_vImag[i1] = this->_vImag[i] - t2; - this->_vReal[i] += t1; - this->_vImag[i] += t2; - } - double z = ((u1 * c1) - (u2 * c2)); - u2 = ((u1 * c2) + (u2 * c1)); - u1 = z; - } -#ifdef __AVR__ - c2 = pgm_read_float_near(&(_c2[index])); - c1 = pgm_read_float_near(&(_c1[index])); - index++; -#else - c2 = sqrt((1.0 - c1) / 2.0); - c1 = sqrt((1.0 + c1) / 2.0); -#endif - if (dir == FFT_FORWARD) { - c2 = -c2; - } - } - // Scaling for reverse transform / - if (dir != FFT_FORWARD) { - for (uint16_t i = 0; i < this->_samples; i++) { - this->_vReal[i] /= this->_samples; - this->_vImag[i] /= this->_samples; - } - } -} - -void arduinoFFT::Compute(double *vReal, double *vImag, uint16_t samples, uint8_t power, uint8_t dir) -{ // Computes in-place complex-to-complex FFT - // Reverse bits - #warning("This method is deprecated and may be removed on future revisions.") - uint16_t j = 0; - for (uint16_t i = 0; i < (samples - 1); i++) { - if (i < j) { - Swap(&vReal[i], &vReal[j]); - if(dir==FFT_REVERSE) - Swap(&vImag[i], &vImag[j]); - } - uint16_t k = (samples >> 1); - while (k <= j) { - j -= k; - k >>= 1; - } - j += k; - } - // Compute the FFT -#ifdef __AVR__ - uint8_t index = 0; -#endif - double c1 = -1.0; - double c2 = 0.0; - uint16_t l2 = 1; - for (uint8_t l = 0; (l < power); l++) { - uint16_t l1 = l2; - l2 <<= 1; - double u1 = 1.0; - double u2 = 0.0; - for (j = 0; j < l1; j++) { - for (uint16_t i = j; i < samples; i += l2) { - uint16_t i1 = i + l1; - double t1 = u1 * vReal[i1] - u2 * vImag[i1]; - double t2 = u1 * vImag[i1] + u2 * vReal[i1]; - vReal[i1] = vReal[i] - t1; - vImag[i1] = vImag[i] - t2; - vReal[i] += t1; - vImag[i] += t2; - } - double z = ((u1 * c1) - (u2 * c2)); - u2 = ((u1 * c2) + (u2 * c1)); - u1 = z; - } -#ifdef __AVR__ - c2 = pgm_read_float_near(&(_c2[index])); - c1 = pgm_read_float_near(&(_c1[index])); - index++; -#else - c2 = sqrt((1.0 - c1) / 2.0); - c1 = sqrt((1.0 + c1) / 2.0); -#endif - if (dir == FFT_FORWARD) { - c2 = -c2; - } - } - // Scaling for reverse transform - if (dir != FFT_FORWARD) { - for (uint16_t i = 0; i < samples; i++) { - vReal[i] /= samples; - vImag[i] /= samples; - } - } -} - -void arduinoFFT::ComplexToMagnitude() -{ // vM is half the size of vReal and vImag - for (uint16_t i = 0; i < this->_samples; i++) { - this->_vReal[i] = sqrt(sq(this->_vReal[i]) + sq(this->_vImag[i])); - } -} - -void arduinoFFT::ComplexToMagnitude(double *vReal, double *vImag, uint16_t samples) -{ // vM is half the size of vReal and vImag - #warning("This method is deprecated and may be removed on future revisions.") - for (uint16_t i = 0; i < samples; i++) { - vReal[i] = sqrt(sq(vReal[i]) + sq(vImag[i])); - } -} - -void arduinoFFT::DCRemoval() -{ - // calculate the mean of vData - double mean = 0; - for (uint16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - mean += this->_vReal[i]; - } - mean /= this->_samples; - // Subtract the mean from vData - for (uint16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - this->_vReal[i] -= mean; - } -} - -void arduinoFFT::DCRemoval(double *vData, uint16_t samples) -{ - // calculate the mean of vData - #warning("This method is deprecated and may be removed on future revisions.") - double mean = 0; - for (uint16_t i = 1; i < ((samples >> 1) + 1); i++) - { - mean += vData[i]; - } - mean /= samples; - // Subtract the mean from vData - for (uint16_t i = 1; i < ((samples >> 1) + 1); i++) - { - vData[i] -= mean; - } -} - -void arduinoFFT::Windowing(uint8_t windowType, uint8_t dir) -{// Weighing factors are computed once before multiple use of FFT -// The weighing function is symetric; half the weighs are recorded - double samplesMinusOne = (double(this->_samples) - 1.0); - for (uint16_t i = 0; i < (this->_samples >> 1); i++) { - double indexMinusOne = double(i); - double ratio = (indexMinusOne / samplesMinusOne); - double weighingFactor = 1.0; - // Compute and record weighting factor - switch (windowType) { - case FFT_WIN_TYP_RECTANGLE: // rectangle (box car) - weighingFactor = 1.0; - break; - case FFT_WIN_TYP_HAMMING: // hamming - weighingFactor = 0.54 - (0.46 * cos(twoPi * ratio)); - break; - case FFT_WIN_TYP_HANN: // hann - weighingFactor = 0.54 * (1.0 - cos(twoPi * ratio)); - break; - case FFT_WIN_TYP_TRIANGLE: // triangle (Bartlett) - weighingFactor = 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / samplesMinusOne); - break; - case FFT_WIN_TYP_NUTTALL: // nuttall - weighingFactor = 0.355768 - (0.487396 * (cos(twoPi * ratio))) + (0.144232 * (cos(fourPi * ratio))) - (0.012604 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN: // blackman - weighingFactor = 0.42323 - (0.49755 * (cos(twoPi * ratio))) + (0.07922 * (cos(fourPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN_NUTTALL: // blackman nuttall - weighingFactor = 0.3635819 - (0.4891775 * (cos(twoPi * ratio))) + (0.1365995 * (cos(fourPi * ratio))) - (0.0106411 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN_HARRIS: // blackman harris - weighingFactor = 0.35875 - (0.48829 * (cos(twoPi * ratio))) + (0.14128 * (cos(fourPi * ratio))) - (0.01168 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_FLT_TOP: // flat top - weighingFactor = 0.2810639 - (0.5208972 * cos(twoPi * ratio)) + (0.1980399 * cos(fourPi * ratio)); - break; - case FFT_WIN_TYP_WELCH: // welch - weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); - break; - } - if (dir == FFT_FORWARD) { - this->_vReal[i] *= weighingFactor; - this->_vReal[this->_samples - (i + 1)] *= weighingFactor; - } - else { - this->_vReal[i] /= weighingFactor; - this->_vReal[this->_samples - (i + 1)] /= weighingFactor; - } - } -} - - -void arduinoFFT::Windowing(double *vData, uint16_t samples, uint8_t windowType, uint8_t dir) -{// Weighing factors are computed once before multiple use of FFT -// The weighing function is symetric; half the weighs are recorded - #warning("This method is deprecated and may be removed on future revisions.") - double samplesMinusOne = (double(samples) - 1.0); - for (uint16_t i = 0; i < (samples >> 1); i++) { - double indexMinusOne = double(i); - double ratio = (indexMinusOne / samplesMinusOne); - double weighingFactor = 1.0; - // Compute and record weighting factor - switch (windowType) { - case FFT_WIN_TYP_RECTANGLE: // rectangle (box car) - weighingFactor = 1.0; - break; - case FFT_WIN_TYP_HAMMING: // hamming - weighingFactor = 0.54 - (0.46 * cos(twoPi * ratio)); - break; - case FFT_WIN_TYP_HANN: // hann - weighingFactor = 0.54 * (1.0 - cos(twoPi * ratio)); - break; - case FFT_WIN_TYP_TRIANGLE: // triangle (Bartlett) - weighingFactor = 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / samplesMinusOne); - break; - case FFT_WIN_TYP_NUTTALL: // nuttall - weighingFactor = 0.355768 - (0.487396 * (cos(twoPi * ratio))) + (0.144232 * (cos(fourPi * ratio))) - (0.012604 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN: // blackman - weighingFactor = 0.42323 - (0.49755 * (cos(twoPi * ratio))) + (0.07922 * (cos(fourPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN_NUTTALL: // blackman nuttall - weighingFactor = 0.3635819 - (0.4891775 * (cos(twoPi * ratio))) + (0.1365995 * (cos(fourPi * ratio))) - (0.0106411 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_BLACKMAN_HARRIS: // blackman harris - weighingFactor = 0.35875 - (0.48829 * (cos(twoPi * ratio))) + (0.14128 * (cos(fourPi * ratio))) - (0.01168 * (cos(sixPi * ratio))); - break; - case FFT_WIN_TYP_FLT_TOP: // flat top - weighingFactor = 0.2810639 - (0.5208972 * cos(twoPi * ratio)) + (0.1980399 * cos(fourPi * ratio)); - break; - case FFT_WIN_TYP_WELCH: // welch - weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); - break; - } - if (dir == FFT_FORWARD) { - vData[i] *= weighingFactor; - vData[samples - (i + 1)] *= weighingFactor; - } - else { - vData[i] /= weighingFactor; - vData[samples - (i + 1)] /= weighingFactor; - } - } -} - -double arduinoFFT::MajorPeak() -{ - double maxY = 0; - uint16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint16_t i = 1; i < ((this->_samples >> 1) + 1); i++) { - if ((this->_vReal[i-1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i+1])) { - if (this->_vReal[i] > maxY) { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - double delta = 0.5 * ((this->_vReal[IndexOfMaxY-1] - this->_vReal[IndexOfMaxY+1]) / (this->_vReal[IndexOfMaxY-1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY+1])); - double interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples-1); - if(IndexOfMaxY==(this->_samples >> 1)) //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - // returned value: interpolated frequency peak apex - return(interpolatedX); -} - -void arduinoFFT::MajorPeak(double *f, double *v) -{ - double maxY = 0; - uint16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint16_t i = 1; i < ((this->_samples >> 1) + 1); i++) { - if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) { - if (this->_vReal[i] > maxY) { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - double delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); - double interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - // returned value: interpolated frequency peak apex - *f = interpolatedX; - *v = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); -} - -double arduinoFFT::MajorPeak(double *vD, uint16_t samples, double samplingFrequency) -{ - #warning("This method is deprecated and may be removed on future revisions.") - double maxY = 0; - uint16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint16_t i = 1; i < ((samples >> 1) + 1); i++) { - if ((vD[i-1] < vD[i]) && (vD[i] > vD[i+1])) { - if (vD[i] > maxY) { - maxY = vD[i]; - IndexOfMaxY = i; - } - } - } - double delta = 0.5 * ((vD[IndexOfMaxY-1] - vD[IndexOfMaxY+1]) / (vD[IndexOfMaxY-1] - (2.0 * vD[IndexOfMaxY]) + vD[IndexOfMaxY+1])); - double interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples-1); - if(IndexOfMaxY==(samples >> 1)) //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples); - // returned value: interpolated frequency peak apex - return(interpolatedX); -} - -void arduinoFFT::MajorPeak(double *vD, uint16_t samples, double samplingFrequency, double *f, double *v) -{ - #warning("This method is deprecated and may be removed on future revisions.") - double maxY = 0; - uint16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint16_t i = 1; i < ((samples >> 1) + 1); i++) { - if ((vD[i - 1] < vD[i]) && (vD[i] > vD[i + 1])) { - if (vD[i] > maxY) { - maxY = vD[i]; - IndexOfMaxY = i; - } - } - } - double delta = 0.5 * ((vD[IndexOfMaxY - 1] - vD[IndexOfMaxY + 1]) / (vD[IndexOfMaxY - 1] - (2.0 * vD[IndexOfMaxY]) + vD[IndexOfMaxY + 1])); - double interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples - 1); - //double popo = - if (IndexOfMaxY == (samples >> 1)) //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples); - // returned value: interpolated frequency peak apex - *f = interpolatedX; - *v = abs(vD[IndexOfMaxY - 1] - (2.0 * vD[IndexOfMaxY]) + vD[IndexOfMaxY + 1]); -} - -uint8_t arduinoFFT::Exponent(uint16_t value) -{ - #warning("This method may not be accessible on future revisions.") - // Calculates the base 2 logarithm of a value - uint8_t result = 0; - while (((value >> result) & 1) != 1) result++; - return(result); -} - -// Private functions - -void arduinoFFT::Swap(double *x, double *y) -{ - double temp = *x; - *x = *y; - *y = temp; -} diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 73927dc..bb67f91 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -3,6 +3,7 @@ FFT libray Copyright (C) 2010 Didier Longueville Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,94 +20,420 @@ */ -#ifndef arduinoFFT_h /* Prevent loading library twice */ -#define arduinoFFT_h +#ifndef ArduinoFFT_h /* Prevent loading library twice */ +#define ArduinoFFT_h #ifdef ARDUINO - #if ARDUINO >= 100 - #include "Arduino.h" - #else - #include "WProgram.h" /* This is where the standard Arduino code lies */ - #endif +#if ARDUINO >= 100 +#include "Arduino.h" #else - #include - #include - #ifdef __AVR__ - #include - #include - #endif - #include - #include "defs.h" - #include "types.h" +#include "WProgram.h" /* This is where the standard Arduino code lies */ #endif - -#define FFT_LIB_REV 0x14 -/* Custom constants */ -#define FFT_FORWARD 0x01 -#define FFT_REVERSE 0x00 - -/* Windowing type */ -#define FFT_WIN_TYP_RECTANGLE 0x00 /* rectangle (Box car) */ -#define FFT_WIN_TYP_HAMMING 0x01 /* hamming */ -#define FFT_WIN_TYP_HANN 0x02 /* hann */ -#define FFT_WIN_TYP_TRIANGLE 0x03 /* triangle (Bartlett) */ -#define FFT_WIN_TYP_NUTTALL 0x04 /* nuttall */ -#define FFT_WIN_TYP_BLACKMAN 0x05 /* blackman */ -#define FFT_WIN_TYP_BLACKMAN_NUTTALL 0x06 /* blackman nuttall */ -#define FFT_WIN_TYP_BLACKMAN_HARRIS 0x07 /* blackman harris*/ -#define FFT_WIN_TYP_FLT_TOP 0x08 /* flat top */ -#define FFT_WIN_TYP_WELCH 0x09 /* welch */ -/*Mathematial constants*/ -#define twoPi 6.28318531 -#define fourPi 12.56637061 -#define sixPi 18.84955593 - +#else +#include +#include #ifdef __AVR__ - static const double _c1[]PROGMEM = {0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, - 0.9951847267, 0.9987954562, 0.9996988187, 0.9999247018, - 0.9999811753, 0.9999952938, 0.9999988235, 0.9999997059, - 0.9999999265, 0.9999999816, 0.9999999954, 0.9999999989, - 0.9999999997}; - static const double _c2[]PROGMEM = {1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, - 0.0980171403, 0.0490676743, 0.0245412285, 0.0122715383, - 0.0061358846, 0.0030679568, 0.0015339802, 0.0007669903, - 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, - 0.0000239684}; +#include +#include #endif -class arduinoFFT { +#include +#include "defs.h" +#include "types.h" +#endif + +// Define this to use reciprocal multiplication for division and some more speedups that might decrease precision +//#define FFT_SPEED_OVER_PRECISION + +#ifndef FFT_SQRT_APPROXIMATION + #define sqrt_internal sqrt +#endif + +// Define this to use a low-precision square root approximation instead of the regular sqrt() call +// This might only work for specific use cases, but is significantly faster. Only works for ArduinoFFT. +//#define FFT_SQRT_APPROXIMATION + +enum class FFTDirection +{ + Reverse, + Forward +}; +enum class FFTWindow +{ + Rectangle, // rectangle (Box car) + Hamming, // hamming + Hann, // hann + Triangle, // triangle (Bartlett) + Nuttall, // nuttall + Blackman, //blackman + Blackman_Nuttall, // blackman nuttall + Blackman_Harris, // blackman harris + Flat_top, // flat top + Welch // welch +}; + +template +class ArduinoFFT +{ public: - /* Constructor */ - arduinoFFT(void); - arduinoFFT(double *vReal, double *vImag, uint16_t samples, double samplingFrequency); - /* Destructor */ - ~arduinoFFT(void); - /* Functions */ - uint8_t Revision(void); - uint8_t Exponent(uint16_t value); + // Constructor + ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * windowWeighingFactors = nullptr) + : _vReal(vReal) + , _vImag(vImag) + , _samples(samples) +#ifdef FFT_SPEED_OVER_PRECISION + , _oneOverSamples(1.0 / samples) +#endif + , _samplingFrequency(samplingFrequency) + , _windowWeighingFactors(windowWeighingFactors) + { + // Calculates the base 2 logarithm of sample count + _power = 0; + while (((samples >> _power) & 1) != 1) + { + _power++; + } + } - void ComplexToMagnitude(double *vReal, double *vImag, uint16_t samples); - void Compute(double *vReal, double *vImag, uint16_t samples, uint8_t dir); - void Compute(double *vReal, double *vImag, uint16_t samples, uint8_t power, uint8_t dir); - void DCRemoval(double *vData, uint16_t samples); - double MajorPeak(double *vD, uint16_t samples, double samplingFrequency); - void MajorPeak(double *vD, uint16_t samples, double samplingFrequency, double *f, double *v); - void Windowing(double *vData, uint16_t samples, uint8_t windowType, uint8_t dir); + // Destructor + ~ArduinoFFT() + { + } - void ComplexToMagnitude(); - void Compute(uint8_t dir); - void DCRemoval(); - double MajorPeak(); - void MajorPeak(double *f, double *v); - void Windowing(uint8_t windowType, uint8_t dir); + // Get library revision + static uint8_t revision() + { + return 0x19; + } + + // Computes in-place complex-to-complex FFT + void compute(FFTDirection dir) const + { + // Reverse bits / + uint_fast16_t j = 0; + for (uint_fast16_t i = 0; i < (this->_samples - 1); i++) + { + if (i < j) + { + Swap(this->_vReal[i], this->_vReal[j]); + if (dir == FFTDirection::Reverse) + { + Swap(this->_vImag[i], this->_vImag[j]); + } + } + uint_fast16_t k = (this->_samples >> 1); + while (k <= j) + { + j -= k; + k >>= 1; + } + j += k; + } + // Compute the FFT +#ifdef __AVR__ + small_type index = 0; +#endif + T c1 = -1.0; + T c2 = 0.0; + uint_fast16_t l2 = 1; + for (uint_fast8_t l = 0; (l < this->_power); l++) + { + uint_fast16_t l1 = l2; + l2 <<= 1; + T u1 = 1.0; + T u2 = 0.0; + for (j = 0; j < l1; j++) + { + for (uint_fast16_t i = j; i < this->_samples; i += l2) + { + uint_fast16_t i1 = i + l1; + T t1 = u1 * this->_vReal[i1] - u2 * this->_vImag[i1]; + T t2 = u1 * this->_vImag[i1] + u2 * this->_vReal[i1]; + this->_vReal[i1] = this->_vReal[i] - t1; + this->_vImag[i1] = this->_vImag[i] - t2; + this->_vReal[i] += t1; + this->_vImag[i] += t2; + } + T z = ((u1 * c1) - (u2 * c2)); + u2 = ((u1 * c2) + (u2 * c1)); + u1 = z; + } +#ifdef __AVR__ + c2 = pgm_read_T_near(&(_c2[index])); + c1 = pgm_read_T_near(&(_c1[index])); + index++; +#else + T cTemp = 0.5 * c1; + c2 = sqrt_internal(0.5 - cTemp); + c1 = sqrt_internal(0.5 + cTemp); +#endif + c2 = dir == FFTDirection::Forward ? -c2 : c2; + } + // Scaling for reverse transform + if (dir != FFTDirection::Forward) + { + for (uint_fast16_t i = 0; i < this->_samples; i++) + { +#ifdef FFT_SPEED_OVER_PRECISION + this->_vReal[i] *= _oneOverSamples; + this->_vImag[i] *= _oneOverSamples; +#else + this->_vReal[i] /= this->_samples; + this->_vImag[i] /= this->_samples; +#endif + } + } + } + + void complexToMagnitude() const + { + // vM is half the size of vReal and vImag + for (uint_fast16_t i = 0; i < this->_samples; i++) + { + this->_vReal[i] = sqrt_internal(sq(this->_vReal[i]) + sq(this->_vImag[i])); + } + } + + void dcRemoval() const + { + // calculate the mean of vData + T mean = 0; + for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) + { + mean += this->_vReal[i]; + } + mean /= this->_samples; + // Subtract the mean from vData + for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) + { + this->_vReal[i] -= mean; + } + } + + void windowing(FFTWindow windowType, FFTDirection dir) + { + // check if values are already pre-computed for the correct window type + if (_windowWeighingFactors && weighingFactorsComputed && weighingFactorsFFTWindow == windowType) + { + // yes. values are precomputed + if (dir == FFTDirection::Forward) + { + for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) + { + this->_vReal[i] *= _windowWeighingFactors[i]; + this->_vReal[this->_samples - (i + 1)] *= _windowWeighingFactors[i]; + } + } + else + { + for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) + { +#ifdef FFT_SPEED_OVER_PRECISION + // on many architectures reciprocals and multiplying are much faster than division + T oneOverFactor = 1.0 / _windowWeighingFactors[i]; + this->_vReal[i] *= oneOverFactor; + this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; +#else + this->_vReal[i] /= _windowWeighingFactors[i]; + this->_vReal[this->_samples - (i + 1)] /= _windowWeighingFactors[i]; +#endif + } + } + } + else + { + // no. values need to be pre-computed or applied + T samplesMinusOne = (T(this->_samples) - 1.0); + for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) + { + T indexMinusOne = T(i); + T ratio = (indexMinusOne / samplesMinusOne); + T weighingFactor = 1.0; + // Compute and record weighting factor + switch (windowType) + { + case FFTWindow::Rectangle: // rectangle (box car) + weighingFactor = 1.0; + break; + case FFTWindow::Hamming: // hamming + weighingFactor = 0.54 - (0.46 * cos(TWO_PI * ratio)); + break; + case FFTWindow::Hann: // hann + weighingFactor = 0.54 * (1.0 - cos(TWO_PI * ratio)); + break; + case FFTWindow::Triangle: // triangle (Bartlett) + weighingFactor = 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / samplesMinusOne); + break; + case FFTWindow::Nuttall: // nuttall + weighingFactor = 0.355768 - (0.487396 * (cos(TWO_PI * ratio))) + (0.144232 * (cos(FOUR_PI * ratio))) - (0.012604 * (cos(SIX_PI * ratio))); + break; + case FFTWindow::Blackman: // blackman + weighingFactor = 0.42323 - (0.49755 * (cos(TWO_PI * ratio))) + (0.07922 * (cos(FOUR_PI * ratio))); + break; + case FFTWindow::Blackman_Nuttall: // blackman nuttall + weighingFactor = 0.3635819 - (0.4891775 * (cos(TWO_PI * ratio))) + (0.1365995 * (cos(FOUR_PI * ratio))) - (0.0106411 * (cos(SIX_PI * ratio))); + break; + case FFTWindow::Blackman_Harris: // blackman harris + weighingFactor = 0.35875 - (0.48829 * (cos(TWO_PI * ratio))) + (0.14128 * (cos(FOUR_PI * ratio))) - (0.01168 * (cos(SIX_PI * ratio))); + break; + case FFTWindow::Flat_top: // flat top + weighingFactor = 0.2810639 - (0.5208972 * cos(TWO_PI * ratio)) + (0.1980399 * cos(FOUR_PI * ratio)); + break; + case FFTWindow::Welch: // welch + weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); + break; + } + if (_windowWeighingFactors) + { + _windowWeighingFactors[i] = weighingFactor; + } + if (dir == FFTDirection::Forward) + { + this->_vReal[i] *= weighingFactor; + this->_vReal[this->_samples - (i + 1)] *= weighingFactor; + } + else + { +#ifdef FFT_SPEED_OVER_PRECISION + // on many architectures reciprocals and multiplying are much faster than division + T oneOverFactor = 1.0 / weighingFactor; + this->_vReal[i] *= oneOverFactor; + this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; +#else + this->_vReal[i] /= weighingFactor; + this->_vReal[this->_samples - (i + 1)] /= weighingFactor; +#endif + } + } + // mark cached values as pre-computed + weighingFactorsFFTWindow = windowType; + weighingFactorsComputed = true; + } + } + + T majorPeak() const + { + T maxY = 0; + uint_fast16_t IndexOfMaxY = 0; + //If sampling_frequency = 2 * max_frequency in signal, + //value would be stored at position samples/2 + for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) + { + if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) + { + if (this->_vReal[i] > maxY) + { + maxY = this->_vReal[i]; + IndexOfMaxY = i; + } + } + } + T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); + T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); + if (IndexOfMaxY == (this->_samples >> 1)) + { + //To improve calculation on edge values + interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); + } + // returned value: interpolated frequency peak apex + return interpolatedX; + } + + void majorPeak(T &f, T &v) const + { + T maxY = 0; + uint_fast16_t IndexOfMaxY = 0; + //If sampling_frequency = 2 * max_frequency in signal, + //value would be stored at position samples/2 + for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) + { + if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) + { + if (this->_vReal[i] > maxY) + { + maxY = this->_vReal[i]; + IndexOfMaxY = i; + } + } + } + T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); + T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); + if (IndexOfMaxY == (this->_samples >> 1)) + { + //To improve calculation on edge values + interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); + } + // returned value: interpolated frequency peak apex + f = interpolatedX; + v = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); + } private: +#ifdef __AVR__ + static const T _c1[] PROGMEM = { + 0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, + 0.9951847267, 0.9987954562, 0.9996988187, 0.9999247018, + 0.9999811753, 0.9999952938, 0.9999988235, 0.9999997059, + 0.9999999265, 0.9999999816, 0.9999999954, 0.9999999989, + 0.9999999997}; + static const T _c2[] PROGMEM = { + 1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, + 0.0980171403, 0.0490676743, 0.0245412285, 0.0122715383, + 0.0061358846, 0.0030679568, 0.0015339802, 0.0007669903, + 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, + 0.0000239684}; +#endif + + // Mathematial constants +#ifndef TWO_PI + static constexpr T TWO_PI = 6.28318531; // might already be defined in Arduino.h +#endif + static constexpr T FOUR_PI = 12.56637061; + static constexpr T SIX_PI = 18.84955593; + + static inline void Swap(T &x, T &y) + { + T temp = x; + x = y; + y = temp; + } + +#ifdef FFT_SQRT_APPROXIMATION + template + static inline V sqrt_internal(typename std::enable_if::value, V>::type x) + { + + union { + int i; + float x; + } u; + u.x = x; + u.i = (1 << 29) + (u.i >> 1) - (1 << 22); + // Two Babylonian Steps (simplified from:) + // u.x = 0.5f * (u.x + x/u.x); + // u.x = 0.5f * (u.x + x/u.x); + u.x = u.x + x / u.x; + u.x = 0.25f * u.x + x / u.x; + return u.x; + } + + template + static inline V sqrt_internal(typename std::enable_if::value, V>::type x) + { + return sqrt(x); + } +#endif + /* Variables */ - uint16_t _samples; - double _samplingFrequency; - double *_vReal; - double *_vImag; - uint8_t _power; - /* Functions */ - void Swap(double *x, double *y); + uint_fast16_t _samples = 0; +#ifdef FFT_SPEED_OVER_PRECISION + T _oneOverSamples = 0.0; +#endif + T _samplingFrequency = 0; + T *_vReal = nullptr; + T *_vImag = nullptr; + T * _windowWeighingFactors = nullptr; + FFTWindow weighingFactorsFFTWindow; + bool weighingFactorsComputed = false; + uint_fast8_t _power = 0; }; #endif From 08e9288cc947934529718ec59d5d9d7895809941 Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Wed, 19 Feb 2020 17:28:01 +0100 Subject: [PATCH 02/17] Fix example FFT_02 --- Examples/FFT_02/FFT_02.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/FFT_02/FFT_02.ino b/Examples/FFT_02/FFT_02.ino index bfa1804..7164dab 100644 --- a/Examples/FFT_02/FFT_02.ino +++ b/Examples/FFT_02/FFT_02.ino @@ -45,7 +45,7 @@ double vImag[samples]; /* Create FFT object */ ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, sampling); -unsigned long time; +unsigned long startTime; #define SCL_INDEX 0x00 #define SCL_TIME 0x01 @@ -73,7 +73,7 @@ void loop() } /*Serial.println("Data:"); PrintVector(vReal, samples, SCL_TIME);*/ - time=millis(); + startTime=millis(); FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ /*Serial.println("Weighed data:"); PrintVector(vReal, samples, SCL_TIME);*/ @@ -90,7 +90,7 @@ void loop() Serial.print(": \t\t"); Serial.print(x, 4); Serial.print("\t\t"); - Serial.print(millis()-time); + Serial.print(millis()-startTime); Serial.println(" ms"); // delay(2000); /* Repeat after delay */ } From 49bc72673808ebe9e20864b6f1bf04aeb53451ae Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Wed, 19 Feb 2020 18:06:11 +0100 Subject: [PATCH 03/17] Add windowing compensation factors --- README.md | 18 +++++++++++++++--- src/arduinoFFT.h | 35 ++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index feb4d7a..fe000f6 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The type `T` can be `float` or `double`. `vReal` and `vImag` are pointers to arr * **~ArduinoFFT**(void); Destructor. * **complexToMagnitude**(); -Convert complex values to their magnitude and store in vReal. +Convert complex values to their magnitude and store in vReal. * **compute**(FFTDirection dir); Calcuates the Fast Fourier Transform. * **dcRemoval**(); @@ -57,7 +57,7 @@ Removes the DC component from the sample data. Looks for and returns the frequency of the biggest spike in the analyzed signal. * **revision**(); Returns the library revision. -* **windowing**(FFTWindow windowType, FFTDirection dir); +* **windowing**(FFTWindow windowType, FFTDirection dir, bool withCompensation = false); Performs a windowing function on the values array. The possible windowing options are: * Rectangle * Hamming @@ -66,10 +66,22 @@ Performs a windowing function on the values array. The possible windowing option * Nuttall * Blackman * Blackman_Nuttall - * Blackman_Harris + * Blackman_Harris * Flat_top * Welch + If `withCompensation` == true, the following compensation factors are used: + * Rectangle: 1.0 * 2.0 + * Hamming: 1.8549343278 * 2.0 + * Hann: 1.8554726898 * 2.0 + * Triangle: 2.0039186079 * 2.0 + * Nuttall: 2.8163172034 * 2.0 + * Blackman: 2.3673474360 * 2.0 + * Blackman Nuttall: 2.7557840395 * 2.0 + * Blackman Harris: 2.7929062517 * 2.0 + * Flat top: 3.5659039231 * 2.0 + * Welch: 1.5029392863 * 2.0 + ## Special flags You can define these before including arduinoFFT.h: diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index bb67f91..d47d6b6 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -208,10 +208,12 @@ public: } } - void windowing(FFTWindow windowType, FFTDirection dir) + void windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false) { - // check if values are already pre-computed for the correct window type - if (_windowWeighingFactors && weighingFactorsComputed && weighingFactorsFFTWindow == windowType) + // check if values are already pre-computed for the correct window type and compensation + if (_windowWeighingFactors && _weighingFactorsComputed && + _weighingFactorsFFTWindow == windowType && + _weighingFactorsWithCompensation == withCompensation) { // yes. values are precomputed if (dir == FFTDirection::Forward) @@ -242,6 +244,7 @@ public: { // no. values need to be pre-computed or applied T samplesMinusOne = (T(this->_samples) - 1.0); + T compensationFactor = _WindowCompensationFactors[static_cast(windowType)]; for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) { T indexMinusOne = T(i); @@ -281,6 +284,10 @@ public: weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); break; } + if (withCompensation) + { + weighingFactor *= compensationFactor; + } if (_windowWeighingFactors) { _windowWeighingFactors[i] = weighingFactor; @@ -304,8 +311,9 @@ public: } } // mark cached values as pre-computed - weighingFactorsFFTWindow = windowType; - weighingFactorsComputed = true; + _weighingFactorsFFTWindow = windowType; + _weighingFactorsWithCompensation = withCompensation; + _weighingFactorsComputed = true; } } @@ -381,6 +389,18 @@ private: 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, 0.0000239684}; #endif + static constexpr T _WindowCompensationFactors[10] = { + 1.0000000000 * 2.0, /* rectangle (Box car) */ + 1.8549343278 * 2.0, /* hamming */ + 1.8554726898 * 2.0, /* hann */ + 2.0039186079 * 2.0, /* triangle (Bartlett) */ + 2.8163172034 * 2.0, /* nuttall */ + 2.3673474360 * 2.0, /* blackman */ + 2.7557840395 * 2.0, /* blackman nuttall */ + 2.7929062517 * 2.0, /* blackman harris*/ + 3.5659039231 * 2.0, /* flat top */ + 1.5029392863 * 2.0 /* welch */ + }; // Mathematial constants #ifndef TWO_PI @@ -431,8 +451,9 @@ private: T *_vReal = nullptr; T *_vImag = nullptr; T * _windowWeighingFactors = nullptr; - FFTWindow weighingFactorsFFTWindow; - bool weighingFactorsComputed = false; + FFTWindow _weighingFactorsFFTWindow; + bool _weighingFactorsWithCompensation = false; + bool _weighingFactorsComputed = false; uint_fast8_t _power = 0; }; From 6df8d2d70fd905856800e2b78a984755ad6c0f9a Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Thu, 20 Feb 2020 20:54:34 +0100 Subject: [PATCH 04/17] Add missing include if compiled from Visual Studio Code + Arduino Extension --- src/arduinoFFT.h | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index d47d6b6..730b05b 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -43,14 +43,16 @@ // Define this to use reciprocal multiplication for division and some more speedups that might decrease precision //#define FFT_SPEED_OVER_PRECISION -#ifndef FFT_SQRT_APPROXIMATION - #define sqrt_internal sqrt -#endif - // Define this to use a low-precision square root approximation instead of the regular sqrt() call // This might only work for specific use cases, but is significantly faster. Only works for ArduinoFFT. //#define FFT_SQRT_APPROXIMATION +#ifdef FFT_SQRT_APPROXIMATION + #include +#else + #define sqrt_internal sqrt +#endif + enum class FFTDirection { Reverse, @@ -390,16 +392,16 @@ private: 0.0000239684}; #endif static constexpr T _WindowCompensationFactors[10] = { - 1.0000000000 * 2.0, /* rectangle (Box car) */ - 1.8549343278 * 2.0, /* hamming */ - 1.8554726898 * 2.0, /* hann */ - 2.0039186079 * 2.0, /* triangle (Bartlett) */ - 2.8163172034 * 2.0, /* nuttall */ - 2.3673474360 * 2.0, /* blackman */ - 2.7557840395 * 2.0, /* blackman nuttall */ - 2.7929062517 * 2.0, /* blackman harris*/ - 3.5659039231 * 2.0, /* flat top */ - 1.5029392863 * 2.0 /* welch */ + 2, /* rectangle (Box car) */ + 1.8549343278 * 2, /* hamming */ + 1.8554726898 * 2, /* hann */ + 2.0039186079 * 2, /* triangle (Bartlett) */ + 2.8163172034 * 2, /* nuttall */ + 2.367347436 * 2, /* blackman */ + 2.7557840395 * 2, /* blackman nuttall */ + 2.7929062517 * 2, /* blackman harris*/ + 3.5659039231 * 2, /* flat top */ + 1.5029392863 * 2 /* welch */ }; // Mathematial constants From 3e73c9884b978afa8ab97b9367c21ba2426c6b33 Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Sat, 22 Feb 2020 12:38:26 +0100 Subject: [PATCH 05/17] Use better sqrtf approximation (precise, no divisions) --- src/arduinoFFT.h | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 730b05b..564df15 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -392,16 +392,16 @@ private: 0.0000239684}; #endif static constexpr T _WindowCompensationFactors[10] = { - 2, /* rectangle (Box car) */ - 1.8549343278 * 2, /* hamming */ - 1.8554726898 * 2, /* hann */ - 2.0039186079 * 2, /* triangle (Bartlett) */ - 2.8163172034 * 2, /* nuttall */ - 2.367347436 * 2, /* blackman */ - 2.7557840395 * 2, /* blackman nuttall */ - 2.7929062517 * 2, /* blackman harris*/ - 3.5659039231 * 2, /* flat top */ - 1.5029392863 * 2 /* welch */ + 1.0000000000 * 2.0, // rectangle (Box car) + 1.8549343278 * 2.0, // hamming + 1.8554726898 * 2.0, // hann + 2.0039186079 * 2.0, // triangle (Bartlett) + 2.8163172034 * 2.0, // nuttall + 2.3673474360 * 2.0, // blackman + 2.7557840395 * 2.0, // blackman nuttall + 2.7929062517 * 2.0, // blackman harris + 3.5659039231 * 2.0, // flat top + 1.5029392863 * 2.0 // welch }; // Mathematial constants @@ -419,24 +419,27 @@ private: } #ifdef FFT_SQRT_APPROXIMATION + // Fast inverse square root aka "Quake 3 fast inverse square root", multiplied by x. + // Uses one iteration of Halley's method for precision. + // See: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Iterative_methods_for_reciprocal_square_roots + // And: https://github.com/HorstBaerbel/approx template static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { - - union { - int i; + union // get bits for float value + { float x; + int32_t i; } u; u.x = x; - u.i = (1 << 29) + (u.i >> 1) - (1 << 22); - // Two Babylonian Steps (simplified from:) - // u.x = 0.5f * (u.x + x/u.x); - // u.x = 0.5f * (u.x + x/u.x); - u.x = u.x + x / u.x; - u.x = 0.25f * u.x + x / u.x; + u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. use 0x5fe6ec85e7de30da for double + float xu = x * u.x; + float xu2 = xu * u.x; + u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy return u.x; } + // At least on the ESP32, the approximation is not faster, so we use the standard function template static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { From 4a36bd2453b2e783d810f71e20c7695631a6ab06 Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Sat, 22 Feb 2020 13:35:09 +0100 Subject: [PATCH 06/17] Add setArrays() function because of issue #32. Add API migration info to README and improve README. --- README.md | 88 ++++++++++++++++++++++++++++++++-------------- changeLog.txt | 7 +++- keywords.txt | 1 + library.json | 2 +- library.properties | 2 +- src/arduinoFFT.h | 13 +++++-- 6 files changed, 81 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fe000f6..d801455 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ arduinoFFT # Fast Fourier Transform for Arduino This is a fork from https://code.google.com/p/makefurt/ which has been abandoned since 2011. -~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. +~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. This is version 2.0 of the library, which has a different [API](#api). See here [how to migrate from 1.x to 2.x](#migrating-from-1.x-to-2.x). Tested on Arduino 1.6.11 and 1.8.10. ## Installation on Arduino @@ -16,49 +16,57 @@ Use the Arduino Library Manager to install and keep it updated. Just look for ar To install this library, just place this entire folder as a subfolder in your Arduino installation. When installed, this library should look like: `Arduino\libraries\arduinoFTT` (this library's folder) -`Arduino\libraries\arduinoFTT\arduinoFTT.h` (the library header file, uses 32 bit floats or 64bit doubles) +`Arduino\libraries\arduinoFTT\src\arduinoFTT.h` (the library header file. include this in your project) `Arduino\libraries\arduinoFTT\keywords.txt` (the syntax coloring file) -`Arduino\libraries\arduinoFTT\examples` (the examples in the "open" menu) +`Arduino\libraries\arduinoFTT\Examples` (the examples in the "open" menu) `Arduino\libraries\arduinoFTT\LICENSE` (GPL license file) `Arduino\libraries\arduinoFTT\README.md` (this file) ## Building on Arduino After this library is installed, you just have to start the Arduino application. -You may see a few warning messages as it's built. - +You may see a few warning messages as it's built. To use this library in a sketch, go to the Sketch | Import Library menu and select arduinoFTT. This will add a corresponding line to the top of your sketch: `#include ` -## TODO -* Ratio table for windowing function. -* Document windowing functions advantages and disadvantages. -* Optimize usage and arguments. -* Add new windowing functions. -* ~~Spectrum table?~~ - ## API -* **ArduinoFFT**(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * weighingFactors = nullptr); +* ```ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * weighingFactors = nullptr);``` Constructor. -The type `T` can be `float` or `double`. `vReal` and `vImag` are pointers to arrays of real and imaginary data and have to be allocated outside of ArduinoFFT. `samples` is the number of samples in `vReal` and `vImag` and `weighingFactors` (if specified). `samplingFrequency` is the sample frequency of the data. `weighingFactors` can optionally be specified to cache weighing factors for the windowing function. This speeds up repeated calls to **windowing()** significantly. +The type `T` can be `float` or `double`. `vReal` and `vImag` are pointers to arrays of real and imaginary data and have to be allocated outside of ArduinoFFT. `samples` is the number of samples in `vReal` and `vImag` and `weighingFactors` (if specified). `samplingFrequency` is the sample frequency of the data. `weighingFactors` can optionally be specified to cache weighing factors for the windowing function. This speeds up repeated calls to **windowing()** significantly. You can deallocate `vReal` and `vImag` after you are done using the library, or only use specific library functions that only need one of those arrays. -* **~ArduinoFFT**(void); +```C++ +const uint32_t nrOfSamples = 1024; +auto real = new float[nrOfSamples]; +auto imag = new float[nrOfSamples]; +auto fft = ArduinoFFT(real, imag, nrOfSamples, 10000); +// ... fill real + imag and use it ... +fft.compute(); +fft.complexToMagnitude(); +delete [] imag; +// ... continue using real and only functions that use real ... +auto peak = fft.majorPeak(); +``` +* ```~ArduinoFFT()``` Destructor. -* **complexToMagnitude**(); -Convert complex values to their magnitude and store in vReal. -* **compute**(FFTDirection dir); -Calcuates the Fast Fourier Transform. -* **dcRemoval**(); -Removes the DC component from the sample data. -* **majorPeak**(); -Looks for and returns the frequency of the biggest spike in the analyzed signal. -* **revision**(); +* ```void complexToMagnitude() const;``` +Convert complex values to their magnitude and store in vReal. Uses vReal and vImag. +* ```void compute(FFTDirection dir) const;``` +Calcuates the Fast Fourier Transform. Uses vReal and vImag. +* ```void dcRemoval() const;``` +Removes the DC component from the sample data. Uses vReal. +* ```T majorPeak() const;``` +Returns the frequency of the biggest spike in the analyzed signal. Uses vReal. +* ```void majorPeak(T &frequency, T &value) const;``` +Returns the frequency and the value of the biggest spike in the analyzed signal. Uses vReal. +* ```uint8_t revision() const;``` Returns the library revision. -* **windowing**(FFTWindow windowType, FFTDirection dir, bool withCompensation = false); -Performs a windowing function on the values array. The possible windowing options are: +* ```void setArrays(T *vReal, T *vImag);``` +Replace the data array pointers. +* ```void windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false);``` +Performs a windowing function on the values array. Uses vReal. The possible windowing options are: * Rectangle * Hamming * Hann @@ -91,3 +99,31 @@ Define this to use reciprocal multiplication for division and some more speedups * #define FFT_SQRT_APPROXIMATION Define this to use a low-precision square root approximation instead of the regular sqrt() call. This might only work for specific use cases, but is significantly faster. Only works if `T == float`. + +See the `FFT_speedup.ino` example in `Examples/FFT_speedup/FFT_speedup.ino`. + +# Migrating from 1.x to 2.x + +* The function signatures where you could pass in pointers were deprecated and have been removed. Pass in pointers to your real / imaginary array in the ArduinoFFT() constructor. If you have the need to replace those pointers during usage of the library (e.g. to free memory) you can do the following: + +```C++ +const uint32_t nrOfSamples = 1024; +auto real = new float[nrOfSamples]; +auto imag = new float[nrOfSamples]; +auto fft = ArduinoFFT(real, imag, nrOfSamples, 10000); +// ... fill real + imag and use it ... +fft.compute(); +fft.complexToMagnitude(); +delete [] real; +// ... replace vReal in library with imag ... +fft.setArrays(imag, nullptr); +// ... keep doin whatever ... +``` +* All function names are camelCase case now (start with lower-case character), e.g. "windowing()" instead of "Windowing()". + +## TODO +* Ratio table for windowing function. +* Document windowing functions advantages and disadvantages. +* Optimize usage and arguments. +* Add new windowing functions. +* ~~Spectrum table?~~ \ No newline at end of file diff --git a/changeLog.txt b/changeLog.txt index b6dc9ad..1888af9 100644 --- a/changeLog.txt +++ b/changeLog.txt @@ -1,8 +1,13 @@ +02/22/20 v1.9.1 +Add setArrays() function because of issue #32. +Add API migration info to README and improve README. +Use better sqrtf() approximation. + 02/19/20 v1.9.0 Remove deprecated API. Consistent renaming of functions to lowercase. Make template to be able to use float or double type (float brings a ~70% speed increase on ESP32). Add option to provide cache for window function weighing factors (~50% speed increase on ESP32). -Add some #defines to enable math approximisations to further speed up code (~50% speed increase on ESP32). +Add some #defines to enable math approximisations to further speed up code (~40% speed increase on ESP32). 01/27/20 v1.5.5 Lookup table for constants c1 and c2 used during FFT comupting. This increases the FFT computing speed in around 5%. diff --git a/keywords.txt b/keywords.txt index c0f1804..3807cdb 100644 --- a/keywords.txt +++ b/keywords.txt @@ -21,6 +21,7 @@ windowing KEYWORD2 exponent KEYWORD2 revision KEYWORD2 majorPeak KEYWORD2 +setArrays KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/library.json b/library.json index e3b8371..a849eb9 100644 --- a/library.json +++ b/library.json @@ -25,7 +25,7 @@ "email": "bim.overbohm@googlemail.com" } ], - "version": "1.9.0", + "version": "1.9.1", "frameworks": ["arduino","mbed","espidf"], "platforms": "*" } diff --git a/library.properties b/library.properties index 986fbe3..0ee368b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=arduinoFFT -version=1.9.0 +version=1.9.1 author=Enrique Condes maintainer=Enrique Condes sentence=A library for implementing floating point Fast Fourier Transform calculations on Arduino. diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 564df15..2dc39fb 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -106,6 +106,13 @@ public: return 0x19; } + // Replace the data array pointers + void setArrays(T *vReal, T *vImag) + { + _vReal = vReal; + _vImag = vImag; + } + // Computes in-place complex-to-complex FFT void compute(FFTDirection dir) const { @@ -347,7 +354,7 @@ public: return interpolatedX; } - void majorPeak(T &f, T &v) const + void majorPeak(T &frequency, T &value) const { T maxY = 0; uint_fast16_t IndexOfMaxY = 0; @@ -372,8 +379,8 @@ public: interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); } // returned value: interpolated frequency peak apex - f = interpolatedX; - v = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); + frequency = interpolatedX; + value = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); } private: From 35ea7e243f09f6b3e9bee1f6e103ed222ae896de Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Sat, 22 Feb 2020 13:39:04 +0100 Subject: [PATCH 07/17] Fix README anchor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d801455..f253041 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ arduinoFFT # Fast Fourier Transform for Arduino This is a fork from https://code.google.com/p/makefurt/ which has been abandoned since 2011. -~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. This is version 2.0 of the library, which has a different [API](#api). See here [how to migrate from 1.x to 2.x](#migrating-from-1.x-to-2.x). +~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. This is version 2.0 of the library, which has a different [API](#api). See here [how to migrate from 1.x to 2.x](#migrating-from-1x-to-2x). Tested on Arduino 1.6.11 and 1.8.10. ## Installation on Arduino From 0a9cd2b4257f4f835d924cafa70b92b23ec5080c Mon Sep 17 00:00:00 2001 From: Bim Overbohm Date: Wed, 1 Jul 2020 14:34:07 +0200 Subject: [PATCH 08/17] Fix compile error on Arduino due to small_type --- README.md | 4 +-- changeLog.txt | 3 ++ library.json | 2 +- library.properties | 2 +- src/arduinoFFT.h | 80 ++++++++++++++++++++++++++-------------------- 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f253041..f9229ef 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ fft.complexToMagnitude(); delete [] real; // ... replace vReal in library with imag ... fft.setArrays(imag, nullptr); -// ... keep doin whatever ... +// ... keep doing whatever ... ``` * All function names are camelCase case now (start with lower-case character), e.g. "windowing()" instead of "Windowing()". @@ -126,4 +126,4 @@ fft.setArrays(imag, nullptr); * Document windowing functions advantages and disadvantages. * Optimize usage and arguments. * Add new windowing functions. -* ~~Spectrum table?~~ \ No newline at end of file +* ~~Spectrum table?~~ diff --git a/changeLog.txt b/changeLog.txt index 1888af9..d49b854 100644 --- a/changeLog.txt +++ b/changeLog.txt @@ -1,3 +1,6 @@ +02/22/20 v1.9.2 +Fix compilation on AVR systems. + 02/22/20 v1.9.1 Add setArrays() function because of issue #32. Add API migration info to README and improve README. diff --git a/library.json b/library.json index a849eb9..6c35419 100644 --- a/library.json +++ b/library.json @@ -25,7 +25,7 @@ "email": "bim.overbohm@googlemail.com" } ], - "version": "1.9.1", + "version": "1.9.2", "frameworks": ["arduino","mbed","espidf"], "platforms": "*" } diff --git a/library.properties b/library.properties index 0ee368b..0a90947 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=arduinoFFT -version=1.9.1 +version=1.9.2 author=Enrique Condes maintainer=Enrique Condes sentence=A library for implementing floating point Fast Fourier Transform calculations on Arduino. diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 2dc39fb..819d48c 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -58,6 +58,7 @@ enum class FFTDirection Reverse, Forward }; + enum class FFTWindow { Rectangle, // rectangle (Box car) @@ -77,7 +78,7 @@ class ArduinoFFT { public: // Constructor - ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * windowWeighingFactors = nullptr) + ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T *windowWeighingFactors = nullptr) : _vReal(vReal) , _vImag(vImag) , _samples(samples) @@ -138,7 +139,7 @@ public: } // Compute the FFT #ifdef __AVR__ - small_type index = 0; + uint_fast8_t index = 0; #endif T c1 = -1.0; T c2 = 0.0; @@ -166,8 +167,8 @@ public: u1 = z; } #ifdef __AVR__ - c2 = pgm_read_T_near(&(_c2[index])); - c1 = pgm_read_T_near(&(_c1[index])); + c2 = pgm_read_float_near(&(_c2[index])); + c1 = pgm_read_float_near(&(_c1[index])); index++; #else T cTemp = 0.5 * c1; @@ -220,8 +221,8 @@ public: void windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false) { // check if values are already pre-computed for the correct window type and compensation - if (_windowWeighingFactors && _weighingFactorsComputed && - _weighingFactorsFFTWindow == windowType && + if (_windowWeighingFactors && _weighingFactorsComputed && + _weighingFactorsFFTWindow == windowType && _weighingFactorsWithCompensation == withCompensation) { // yes. values are precomputed @@ -373,7 +374,7 @@ public: } T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) + if (IndexOfMaxY == (this->_samples >> 1)) { //To improve calculation on edge values interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); @@ -385,31 +386,10 @@ public: private: #ifdef __AVR__ - static const T _c1[] PROGMEM = { - 0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, - 0.9951847267, 0.9987954562, 0.9996988187, 0.9999247018, - 0.9999811753, 0.9999952938, 0.9999988235, 0.9999997059, - 0.9999999265, 0.9999999816, 0.9999999954, 0.9999999989, - 0.9999999997}; - static const T _c2[] PROGMEM = { - 1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, - 0.0980171403, 0.0490676743, 0.0245412285, 0.0122715383, - 0.0061358846, 0.0030679568, 0.0015339802, 0.0007669903, - 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, - 0.0000239684}; + static const float _c1[] PROGMEM; + static const float _c2[] PROGMEM; #endif - static constexpr T _WindowCompensationFactors[10] = { - 1.0000000000 * 2.0, // rectangle (Box car) - 1.8549343278 * 2.0, // hamming - 1.8554726898 * 2.0, // hann - 2.0039186079 * 2.0, // triangle (Bartlett) - 2.8163172034 * 2.0, // nuttall - 2.3673474360 * 2.0, // blackman - 2.7557840395 * 2.0, // blackman nuttall - 2.7929062517 * 2.0, // blackman harris - 3.5659039231 * 2.0, // flat top - 1.5029392863 * 2.0 // welch - }; + static const T _WindowCompensationFactors[10]; // Mathematial constants #ifndef TWO_PI @@ -430,7 +410,7 @@ private: // Uses one iteration of Halley's method for precision. // See: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Iterative_methods_for_reciprocal_square_roots // And: https://github.com/HorstBaerbel/approx - template + template static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { union // get bits for float value @@ -447,7 +427,7 @@ private: } // At least on the ESP32, the approximation is not faster, so we use the standard function - template + template static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { return sqrt(x); @@ -462,11 +442,43 @@ private: T _samplingFrequency = 0; T *_vReal = nullptr; T *_vImag = nullptr; - T * _windowWeighingFactors = nullptr; + T *_windowWeighingFactors = nullptr; FFTWindow _weighingFactorsFFTWindow; bool _weighingFactorsWithCompensation = false; bool _weighingFactorsComputed = false; uint_fast8_t _power = 0; }; +#ifdef __AVR__ +template +const float ArduinoFFT::_c1[] PROGMEM = { + 0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, + 0.9951847267, 0.9987954562, 0.9996988187, 0.9999247018, + 0.9999811753, 0.9999952938, 0.9999988235, 0.9999997059, + 0.9999999265, 0.9999999816, 0.9999999954, 0.9999999989, + 0.9999999997}; + +template +const float ArduinoFFT::_c2[] PROGMEM = { + 1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, + 0.0980171403, 0.0490676743, 0.0245412285, 0.0122715383, + 0.0061358846, 0.0030679568, 0.0015339802, 0.0007669903, + 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, + 0.0000239684}; +#endif + +template +const T ArduinoFFT::_WindowCompensationFactors[10] = { + 1.0000000000 * 2.0, // rectangle (Box car) + 1.8549343278 * 2.0, // hamming + 1.8554726898 * 2.0, // hann + 2.0039186079 * 2.0, // triangle (Bartlett) + 2.8163172034 * 2.0, // nuttall + 2.3673474360 * 2.0, // blackman + 2.7557840395 * 2.0, // blackman nuttall + 2.7929062517 * 2.0, // blackman harris + 3.5659039231 * 2.0, // flat top + 1.5029392863 * 2.0 // welch +}; + #endif From 7b107cf4907e6e33b754fcb06f454a88c312e984 Mon Sep 17 00:00:00 2001 From: Drzony Date: Fri, 6 Nov 2020 10:11:51 +0100 Subject: [PATCH 09/17] Fixed compilation with -Wextra --- src/arduinoFFT.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 819d48c..b6523f3 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -435,13 +435,13 @@ private: #endif /* Variables */ + T *_vReal = nullptr; + T *_vImag = nullptr; uint_fast16_t _samples = 0; #ifdef FFT_SPEED_OVER_PRECISION T _oneOverSamples = 0.0; #endif T _samplingFrequency = 0; - T *_vReal = nullptr; - T *_vImag = nullptr; T *_windowWeighingFactors = nullptr; FFTWindow _weighingFactorsFFTWindow; bool _weighingFactorsWithCompensation = false; From 3fce8acb8822d1850bcd5e508418ecf86e6a2cf1 Mon Sep 17 00:00:00 2001 From: blaz-r Date: Mon, 13 Sep 2021 08:59:30 +0200 Subject: [PATCH 10/17] Reordered variables in class to fix -Werror=reorder --- src/arduinoFFT.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index 819d48c..483a739 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -435,13 +435,13 @@ private: #endif /* Variables */ - uint_fast16_t _samples = 0; #ifdef FFT_SPEED_OVER_PRECISION T _oneOverSamples = 0.0; #endif - T _samplingFrequency = 0; T *_vReal = nullptr; T *_vImag = nullptr; + uint_fast16_t _samples = 0; + T _samplingFrequency = 0; T *_windowWeighingFactors = nullptr; FFTWindow _weighingFactorsFFTWindow; bool _weighingFactorsWithCompensation = false; From 11b157184ee77ee0da83e8b37a35f65277735ecb Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Mon, 26 Sep 2022 16:39:19 -0500 Subject: [PATCH 11/17] Correctly detect ESP32 boards --- src/arduinoFFT.h | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index b6523f3..4ff7059 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -1,6 +1,6 @@ /* - FFT libray + FFT library Copyright (C) 2010 Didier Longueville Copyright (C) 2014 Enrique Condes Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) @@ -419,18 +419,32 @@ private: int32_t i; } u; u.x = x; - u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. use 0x5fe6ec85e7de30da for double + u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. float xu = x * u.x; float xu2 = xu * u.x; u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy return u.x; } - // At least on the ESP32, the approximation is not faster, so we use the standard function template - static inline V sqrt_internal(typename std::enable_if::value, V>::type x) + static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { + // According to HosrtBaerbel, on the ESP32 the approximation is not faster, so we use the standard function + #ifdef ESP32 return sqrt(x); + #else + union // get bits for float value + { + float x; + int32_t i; + } u; + u.x = x; + u.i = 0x5fe6ec85e7de30da - (u.i >> 1); // gives initial guess y0. + float xu = x * u.x; + float xu2 = xu * u.x; + u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy + return u.x; + #endif } #endif From 8c925a74fd1747bbccb5e8a252c9361e842a58f6 Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Mon, 26 Sep 2022 17:07:55 -0500 Subject: [PATCH 12/17] Correctly detect ESP32 boards --- src/arduinoFFT.h | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index b6523f3..a8dbeb8 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -1,6 +1,6 @@ /* - FFT libray + FFT library Copyright (C) 2010 Didier Longueville Copyright (C) 2014 Enrique Condes Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) @@ -419,18 +419,32 @@ private: int32_t i; } u; u.x = x; - u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. use 0x5fe6ec85e7de30da for double + u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. float xu = x * u.x; float xu2 = xu * u.x; u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy return u.x; } - // At least on the ESP32, the approximation is not faster, so we use the standard function template - static inline V sqrt_internal(typename std::enable_if::value, V>::type x) + static inline V sqrt_internal(typename std::enable_if::value, V>::type x) { + // According to HosrtBaerbel, on the ESP32 the approximation is not faster, so we use the standard function + #ifdef ESP32 return sqrt(x); + #else + union // get bits for float value + { + double x; + int64_t i; + } u; + u.x = x; + u.i = 0x5fe6ec85e7de30da - (u.i >> 1); // gives initial guess y0. + double xu = x * u.x; + double xu2 = xu * u.x; + u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy + return u.x; + #endif } #endif From 3a637a12f62f6b5c95665921d74a87fb7fa43d91 Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Mon, 26 Sep 2022 17:13:00 -0500 Subject: [PATCH 13/17] Merge branch 'develop' of github.com:kosme/arduinoFFT into develop --- src/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/.gitignore diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..00e95bf --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +/arduinoFFT.h.gch From 81d62e10028cefeddee3670ffdea6130147e1760 Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Mon, 26 Sep 2022 18:32:13 -0500 Subject: [PATCH 14/17] Delete arduinoFFT.cpp --- src/arduinoFFT.cpp | 333 --------------------------------------------- 1 file changed, 333 deletions(-) delete mode 100644 src/arduinoFFT.cpp diff --git a/src/arduinoFFT.cpp b/src/arduinoFFT.cpp deleted file mode 100644 index 329952a..0000000 --- a/src/arduinoFFT.cpp +++ /dev/null @@ -1,333 +0,0 @@ -/* - FFT library - Copyright (C) 2010 Didier Longueville - Copyright (C) 2014 Enrique Condes - Copyright (C) 2020 Bim Overbohm (template, speed improvements) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -*/ - -#include "arduinoFFT.h" - -template -ArduinoFFT::ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T *windowWeighingFactors = nullptr) - : _vReal(vReal), _vImag(vImag), _samples(samples) -#ifdef FFT_SPEED_OVER_PRECISION - , - _oneOverSamples(1.0 / samples) -#endif - , - _samplingFrequency(samplingFrequency), _windowWeighingFactors(windowWeighingFactors) -{ - // Calculates the base 2 logarithm of sample count - _power = 0; - while (((samples >> _power) & 1) != 1) - { - _power++; - } -} - -template -ArduinoFFT::~ArduinoFFT(void) -{ -} - -template -uint8_t ArduinoFFT::revision(void) -{ - return 0x19; -} - -template -void ArduinoFFT::setArrays(T *vReal, T *vImag) -{ - _vReal = vReal; - _vImag = vImag; -} - -template -void ArduinoFFT::compute(FFTDirection dir) const -{ - // Reverse bits / - uint_fast16_t j = 0; - for (uint_fast16_t i = 0; i < (this->_samples - 1); i++) - { - if (i < j) - { - Swap(this->_vReal[i], this->_vReal[j]); - if (dir == FFTDirection::Reverse) - { - Swap(this->_vImag[i], this->_vImag[j]); - } - } - uint_fast16_t k = (this->_samples >> 1); - while (k <= j) - { - j -= k; - k >>= 1; - } - j += k; - } - // Compute the FFT -#ifdef __AVR__ - uint_fast8_t index = 0; -#endif - T c1 = -1.0; - T c2 = 0.0; - uint_fast16_t l2 = 1; - for (uint_fast8_t l = 0; (l < this->_power); l++) - { - uint_fast16_t l1 = l2; - l2 <<= 1; - T u1 = 1.0; - T u2 = 0.0; - for (j = 0; j < l1; j++) - { - for (uint_fast16_t i = j; i < this->_samples; i += l2) - { - uint_fast16_t i1 = i + l1; - T t1 = u1 * this->_vReal[i1] - u2 * this->_vImag[i1]; - T t2 = u1 * this->_vImag[i1] + u2 * this->_vReal[i1]; - this->_vReal[i1] = this->_vReal[i] - t1; - this->_vImag[i1] = this->_vImag[i] - t2; - this->_vReal[i] += t1; - this->_vImag[i] += t2; - } - T z = ((u1 * c1) - (u2 * c2)); - u2 = ((u1 * c2) + (u2 * c1)); - u1 = z; - } -#ifdef __AVR__ - c2 = pgm_read_float_near(&(_c2[index])); - c1 = pgm_read_float_near(&(_c1[index])); - index++; -#else - T cTemp = 0.5 * c1; - c2 = sqrt_internal(0.5 - cTemp); - c1 = sqrt_internal(0.5 + cTemp); -#endif - c2 = dir == FFTDirection::Forward ? -c2 : c2; - } - // Scaling for reverse transform - if (dir != FFTDirection::Forward) - { - for (uint_fast16_t i = 0; i < this->_samples; i++) - { -#ifdef FFT_SPEED_OVER_PRECISION - this->_vReal[i] *= _oneOverSamples; - this->_vImag[i] *= _oneOverSamples; -#else - this->_vReal[i] /= this->_samples; - this->_vImag[i] /= this->_samples; -#endif - } - } -} - -template -void ArduinoFFT::complexToMagnitude() const -{ - // vM is half the size of vReal and vImag - for (uint_fast16_t i = 0; i < this->_samples; i++) - { - this->_vReal[i] = sqrt_internal(sq(this->_vReal[i]) + sq(this->_vImag[i])); - } -} - -template -void ArduinoFFT::dcRemoval() const -{ - // calculate the mean of vData - T mean = 0; - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - mean += this->_vReal[i]; - } - mean /= this->_samples; - // Subtract the mean from vData - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - this->_vReal[i] -= mean; - } -} - -template -void ArduinoFFT::windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false) -{ - // check if values are already pre-computed for the correct window type and compensation - if (_windowWeighingFactors && _weighingFactorsComputed && - _weighingFactorsFFTWindow == windowType && - _weighingFactorsWithCompensation == withCompensation) - { - // yes. values are precomputed - if (dir == FFTDirection::Forward) - { - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { - this->_vReal[i] *= _windowWeighingFactors[i]; - this->_vReal[this->_samples - (i + 1)] *= _windowWeighingFactors[i]; - } - } - else - { - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { -#ifdef FFT_SPEED_OVER_PRECISION - // on many architectures reciprocals and multiplying are much faster than division - T oneOverFactor = 1.0 / _windowWeighingFactors[i]; - this->_vReal[i] *= oneOverFactor; - this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; -#else - this->_vReal[i] /= _windowWeighingFactors[i]; - this->_vReal[this->_samples - (i + 1)] /= _windowWeighingFactors[i]; -#endif - } - } - } - else - { - // no. values need to be pre-computed or applied - T samplesMinusOne = (T(this->_samples) - 1.0); - T compensationFactor = _WindowCompensationFactors[static_cast(windowType)]; - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { - T indexMinusOne = T(i); - T ratio = (indexMinusOne / samplesMinusOne); - T weighingFactor = 1.0; - // Compute and record weighting factor - switch (windowType) - { - case FFTWindow::Rectangle: // rectangle (box car) - weighingFactor = 1.0; - break; - case FFTWindow::Hamming: // hamming - weighingFactor = 0.54 - (0.46 * cos(TWO_PI * ratio)); - break; - case FFTWindow::Hann: // hann - weighingFactor = 0.54 * (1.0 - cos(TWO_PI * ratio)); - break; - case FFTWindow::Triangle: // triangle (Bartlett) - weighingFactor = 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / samplesMinusOne); - break; - case FFTWindow::Nuttall: // nuttall - weighingFactor = 0.355768 - (0.487396 * (cos(TWO_PI * ratio))) + (0.144232 * (cos(FOUR_PI * ratio))) - (0.012604 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Blackman: // blackman - weighingFactor = 0.42323 - (0.49755 * (cos(TWO_PI * ratio))) + (0.07922 * (cos(FOUR_PI * ratio))); - break; - case FFTWindow::Blackman_Nuttall: // blackman nuttall - weighingFactor = 0.3635819 - (0.4891775 * (cos(TWO_PI * ratio))) + (0.1365995 * (cos(FOUR_PI * ratio))) - (0.0106411 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Blackman_Harris: // blackman harris - weighingFactor = 0.35875 - (0.48829 * (cos(TWO_PI * ratio))) + (0.14128 * (cos(FOUR_PI * ratio))) - (0.01168 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Flat_top: // flat top - weighingFactor = 0.2810639 - (0.5208972 * cos(TWO_PI * ratio)) + (0.1980399 * cos(FOUR_PI * ratio)); - break; - case FFTWindow::Welch: // welch - weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); - break; - } - if (withCompensation) - { - weighingFactor *= compensationFactor; - } - if (_windowWeighingFactors) - { - _windowWeighingFactors[i] = weighingFactor; - } - if (dir == FFTDirection::Forward) - { - this->_vReal[i] *= weighingFactor; - this->_vReal[this->_samples - (i + 1)] *= weighingFactor; - } - else - { -#ifdef FFT_SPEED_OVER_PRECISION - // on many architectures reciprocals and multiplying are much faster than division - T oneOverFactor = 1.0 / weighingFactor; - this->_vReal[i] *= oneOverFactor; - this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; -#else - this->_vReal[i] /= weighingFactor; - this->_vReal[this->_samples - (i + 1)] /= weighingFactor; -#endif - } - } - // mark cached values as pre-computed - _weighingFactorsFFTWindow = windowType; - _weighingFactorsWithCompensation = withCompensation; - _weighingFactorsComputed = true; - } -} - -template -T ArduinoFFT::majorPeak() const -{ - T maxY = 0; - uint_fast16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) - { - if (this->_vReal[i] > maxY) - { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); - T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) - { - //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - } - // returned value: interpolated frequency peak apex - return interpolatedX; -} - -template -void ArduinoFFT::majorPeak(T &frequency, T &value) const -{ - T maxY = 0; - uint_fast16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) - { - if (this->_vReal[i] > maxY) - { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); - T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) - { - //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - } - // returned value: interpolated frequency peak apex - frequency = interpolatedX; - value = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); -} \ No newline at end of file From e5e4c745c1a61d2c49a9f9e4602769a28d4b11b6 Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Mon, 26 Sep 2022 18:32:38 -0500 Subject: [PATCH 15/17] Delete lookup-table.ods --- lookup-table.ods | Bin 75378 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lookup-table.ods diff --git a/lookup-table.ods b/lookup-table.ods deleted file mode 100644 index b149c9dd33da5dcf12b90dd54bd3b0f7381372a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75378 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 From 5c652cdc956772c5c0ab0a1c8382d09fc23f0fa2 Mon Sep 17 00:00:00 2001 From: Finlay Davidson Date: Mon, 3 Apr 2023 22:00:36 +0200 Subject: [PATCH 16/17] Make the sqrt_internal macro customisable --- src/arduinoFFT.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index a8dbeb8..cb968ff 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -50,7 +50,9 @@ #ifdef FFT_SQRT_APPROXIMATION #include #else - #define sqrt_internal sqrt + #ifndef sqrt_internal + #define sqrt_internal sqrt + #endif #endif enum class FFTDirection From a9f64fb886709706b07605a5154da6af8b2ebe8f Mon Sep 17 00:00:00 2001 From: Enrique Condes Date: Wed, 6 Mar 2024 13:56:17 +0800 Subject: [PATCH 17/17] Version 2.0 --- Examples/FFT_01/FFT_01.ino | 8 +- Examples/FFT_02/FFT_02.ino | 12 +- Examples/FFT_03/FFT_03.ino | 2 +- Examples/FFT_04/FFT_04.ino | 8 +- Examples/FFT_05/FFT_05.ino | 8 +- Examples/FFT_speedup/FFT_speedup.ino | 12 +- README.md | 113 +----- changeLog.txt | 40 -- keywords.txt | 19 +- library.json | 2 +- library.properties | 2 +- src/arduinoFFT.cpp | 518 ++++++++++++++++++++++++ src/arduinoFFT.h | 572 ++++++--------------------- 13 files changed, 688 insertions(+), 628 deletions(-) delete mode 100644 changeLog.txt create mode 100644 src/arduinoFFT.cpp diff --git a/Examples/FFT_01/FFT_01.ino b/Examples/FFT_01/FFT_01.ino index 22b5024..e16368a 100644 --- a/Examples/FFT_01/FFT_01.ino +++ b/Examples/FFT_01/FFT_01.ino @@ -3,7 +3,7 @@ Example of use of the FFT libray Copyright (C) 2014 Enrique Condes - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -64,11 +64,11 @@ void setup() void loop() { /* Build raw data */ - double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read + double ratio = twoPi * signalFrequency / samplingFrequency; // Fraction of a complete cycle stored at each sample (in radians) for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ - //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ + vReal[i] = int8_t(amplitude * sin(i * ratio) / 2.0);/* Build data with positive and negative values*/ + //vReal[i] = uint8_t((amplitude * (sin(i * ratio) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } /* Print the results of the simulated sampling according to time */ diff --git a/Examples/FFT_02/FFT_02.ino b/Examples/FFT_02/FFT_02.ino index 7164dab..a8cbc63 100644 --- a/Examples/FFT_02/FFT_02.ino +++ b/Examples/FFT_02/FFT_02.ino @@ -1,12 +1,12 @@ /* Example of use of the FFT libray to compute FFT for several signals over a range of frequencies. - The exponent is calculated once before the excecution since it is a constant. - This saves resources during the excecution of the sketch and reduces the compiled size. - The sketch shows the time that the computing is taking. + The exponent is calculated once before the excecution since it is a constant. + This saves resources during the excecution of the sketch and reduces the compiled size. + The sketch shows the time that the computing is taking. Copyright (C) 2014 Enrique Condes - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -65,10 +65,10 @@ void loop() for(double frequency = startFrequency; frequency<=stopFrequency; frequency+=step_size) { /* Build raw data */ - double cycles = (((samples-1) * frequency) / sampling); + double ratio = twoPi * frequency / sampling; // Fraction of a complete cycle stored at each sample (in radians) for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0); + vReal[i] = int8_t(amplitude * sin(i * ratio) / 2.0);/* Build data with positive and negative values*/ vImag[i] = 0; //Reset the imaginary values vector for each new frequency } /*Serial.println("Data:"); diff --git a/Examples/FFT_03/FFT_03.ino b/Examples/FFT_03/FFT_03.ino index 2e50613..67ed135 100644 --- a/Examples/FFT_03/FFT_03.ino +++ b/Examples/FFT_03/FFT_03.ino @@ -3,7 +3,7 @@ Example of use of the FFT libray to compute FFT for a signal sampled through the ADC. Copyright (C) 2018 Enrique Condés and Ragnar Ranøyen Homb - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Examples/FFT_04/FFT_04.ino b/Examples/FFT_04/FFT_04.ino index b125991..3934c10 100644 --- a/Examples/FFT_04/FFT_04.ino +++ b/Examples/FFT_04/FFT_04.ino @@ -3,7 +3,7 @@ Example of use of the FFT libray Copyright (C) 2018 Enrique Condes - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -63,11 +63,11 @@ void setup() void loop() { /* Build raw data */ - double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read + double ratio = twoPi * signalFrequency / samplingFrequency; // Fraction of a complete cycle stored at each sample (in radians) for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ - //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ + vReal[i] = int8_t(amplitude * sin(i * ratio) / 2.0);/* Build data with positive and negative values*/ + //vReal[i] = uint8_t((amplitude * (sin(i * ratio) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); /* Weigh data */ diff --git a/Examples/FFT_05/FFT_05.ino b/Examples/FFT_05/FFT_05.ino index a6f4df7..4354fcd 100644 --- a/Examples/FFT_05/FFT_05.ino +++ b/Examples/FFT_05/FFT_05.ino @@ -3,7 +3,7 @@ Example of use of the FFT libray Copyright (C) 2014 Enrique Condes - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -65,11 +65,11 @@ void setup() void loop() { /* Build raw data */ - double cycles = (((samples-1) * signalFrequency) / samplingFrequency); //Number of signal cycles that the sampling will read + double ratio = twoPi * signalFrequency / samplingFrequency; // Fraction of a complete cycle stored at each sample (in radians) for (uint16_t i = 0; i < samples; i++) { - vReal[i] = int8_t((amplitude * (sin((i * (TWO_PI * cycles)) / samples))) / 2.0);/* Build data with positive and negative values*/ - //vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ + vReal[i] = int8_t(amplitude * sin(i * ratio) / 2.0);/* Build data with positive and negative values*/ + //vReal[i] = uint8_t((amplitude * (sin(i * ratio) + 1.0)) / 2.0);/* Build data displaced on the Y axis to include only positive values*/ vImag[i] = 0.0; //Imaginary part must be zeroed in case of looping to avoid wrong calculations and overflows } /* Print the results of the simulated sampling according to time */ diff --git a/Examples/FFT_speedup/FFT_speedup.ino b/Examples/FFT_speedup/FFT_speedup.ino index a059a17..e91a2fe 100644 --- a/Examples/FFT_speedup/FFT_speedup.ino +++ b/Examples/FFT_speedup/FFT_speedup.ino @@ -1,9 +1,9 @@ /* Example of use of the FFT libray to compute FFT for a signal sampled through the ADC - with speedup through different arduinoFFT options. Based on examples/FFT_03/FFT_03.ino + with speedup through different arduinoFFT options. Based on examples/FFT_03/FFT_03.ino - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + Copyright (C) 2020 Bim Overbohm (template, speed improvements) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -47,14 +47,8 @@ Input vectors receive computed results from FFT float vReal[samples]; float vImag[samples]; -/* -Allocate space for FFT window weighing factors, so they are calculated only the first time windowing() is called. -If you don't do this, a lot of calculations are necessary, depending on the window function. -*/ -float weighingFactors[samples]; - /* Create FFT object with weighing factor storage */ -ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency, weighingFactors); +ArduinoFFT FFT = ArduinoFFT(vReal, vImag, samples, samplingFrequency, true); #define SCL_INDEX 0x00 #define SCL_TIME 0x01 diff --git a/README.md b/README.md index f9229ef..dbafd42 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,10 @@ arduinoFFT # Fast Fourier Transform for Arduino This is a fork from https://code.google.com/p/makefurt/ which has been abandoned since 2011. -~~This is a C++ library for Arduino for computing FFT.~~ Now it works both on Arduino and C projects. This is version 2.0 of the library, which has a different [API](#api). See here [how to migrate from 1.x to 2.x](#migrating-from-1x-to-2x). -Tested on Arduino 1.6.11 and 1.8.10. + +This is version 2.0 of the library, which has a different [API](#api). + +Tested on Arduino 1.8.19 and 2.3.2. ## Installation on Arduino @@ -15,17 +17,17 @@ Use the Arduino Library Manager to install and keep it updated. Just look for ar To install this library, just place this entire folder as a subfolder in your Arduino installation. When installed, this library should look like: -`Arduino\libraries\arduinoFTT` (this library's folder) -`Arduino\libraries\arduinoFTT\src\arduinoFTT.h` (the library header file. include this in your project) -`Arduino\libraries\arduinoFTT\keywords.txt` (the syntax coloring file) -`Arduino\libraries\arduinoFTT\Examples` (the examples in the "open" menu) -`Arduino\libraries\arduinoFTT\LICENSE` (GPL license file) +`Arduino\libraries\arduinoFTT` (this library's folder) +`Arduino\libraries\arduinoFTT\src\arduinoFTT.h` (the library header file. include this in your project) +`Arduino\libraries\arduinoFTT\keywords.txt` (the syntax coloring file) +`Arduino\libraries\arduinoFTT\Examples` (the examples in the "open" menu) +`Arduino\libraries\arduinoFTT\LICENSE` (GPL license file) `Arduino\libraries\arduinoFTT\README.md` (this file) ## Building on Arduino After this library is installed, you just have to start the Arduino application. -You may see a few warning messages as it's built. +You may see a few warning messages as it's built. To use this library in a sketch, go to the Sketch | Import Library menu and select arduinoFTT. This will add a corresponding line to the top of your sketch: @@ -33,97 +35,4 @@ select arduinoFTT. This will add a corresponding line to the top of your sketch ## API -* ```ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T * weighingFactors = nullptr);``` -Constructor. -The type `T` can be `float` or `double`. `vReal` and `vImag` are pointers to arrays of real and imaginary data and have to be allocated outside of ArduinoFFT. `samples` is the number of samples in `vReal` and `vImag` and `weighingFactors` (if specified). `samplingFrequency` is the sample frequency of the data. `weighingFactors` can optionally be specified to cache weighing factors for the windowing function. This speeds up repeated calls to **windowing()** significantly. You can deallocate `vReal` and `vImag` after you are done using the library, or only use specific library functions that only need one of those arrays. - -```C++ -const uint32_t nrOfSamples = 1024; -auto real = new float[nrOfSamples]; -auto imag = new float[nrOfSamples]; -auto fft = ArduinoFFT(real, imag, nrOfSamples, 10000); -// ... fill real + imag and use it ... -fft.compute(); -fft.complexToMagnitude(); -delete [] imag; -// ... continue using real and only functions that use real ... -auto peak = fft.majorPeak(); -``` -* ```~ArduinoFFT()``` -Destructor. -* ```void complexToMagnitude() const;``` -Convert complex values to their magnitude and store in vReal. Uses vReal and vImag. -* ```void compute(FFTDirection dir) const;``` -Calcuates the Fast Fourier Transform. Uses vReal and vImag. -* ```void dcRemoval() const;``` -Removes the DC component from the sample data. Uses vReal. -* ```T majorPeak() const;``` -Returns the frequency of the biggest spike in the analyzed signal. Uses vReal. -* ```void majorPeak(T &frequency, T &value) const;``` -Returns the frequency and the value of the biggest spike in the analyzed signal. Uses vReal. -* ```uint8_t revision() const;``` -Returns the library revision. -* ```void setArrays(T *vReal, T *vImag);``` -Replace the data array pointers. -* ```void windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false);``` -Performs a windowing function on the values array. Uses vReal. The possible windowing options are: - * Rectangle - * Hamming - * Hann - * Triangle - * Nuttall - * Blackman - * Blackman_Nuttall - * Blackman_Harris - * Flat_top - * Welch - - If `withCompensation` == true, the following compensation factors are used: - * Rectangle: 1.0 * 2.0 - * Hamming: 1.8549343278 * 2.0 - * Hann: 1.8554726898 * 2.0 - * Triangle: 2.0039186079 * 2.0 - * Nuttall: 2.8163172034 * 2.0 - * Blackman: 2.3673474360 * 2.0 - * Blackman Nuttall: 2.7557840395 * 2.0 - * Blackman Harris: 2.7929062517 * 2.0 - * Flat top: 3.5659039231 * 2.0 - * Welch: 1.5029392863 * 2.0 - -## Special flags - -You can define these before including arduinoFFT.h: - -* #define FFT_SPEED_OVER_PRECISION -Define this to use reciprocal multiplication for division and some more speedups that might decrease precision. - -* #define FFT_SQRT_APPROXIMATION -Define this to use a low-precision square root approximation instead of the regular sqrt() call. This might only work for specific use cases, but is significantly faster. Only works if `T == float`. - -See the `FFT_speedup.ino` example in `Examples/FFT_speedup/FFT_speedup.ino`. - -# Migrating from 1.x to 2.x - -* The function signatures where you could pass in pointers were deprecated and have been removed. Pass in pointers to your real / imaginary array in the ArduinoFFT() constructor. If you have the need to replace those pointers during usage of the library (e.g. to free memory) you can do the following: - -```C++ -const uint32_t nrOfSamples = 1024; -auto real = new float[nrOfSamples]; -auto imag = new float[nrOfSamples]; -auto fft = ArduinoFFT(real, imag, nrOfSamples, 10000); -// ... fill real + imag and use it ... -fft.compute(); -fft.complexToMagnitude(); -delete [] real; -// ... replace vReal in library with imag ... -fft.setArrays(imag, nullptr); -// ... keep doing whatever ... -``` -* All function names are camelCase case now (start with lower-case character), e.g. "windowing()" instead of "Windowing()". - -## TODO -* Ratio table for windowing function. -* Document windowing functions advantages and disadvantages. -* Optimize usage and arguments. -* Add new windowing functions. -* ~~Spectrum table?~~ +Documentation was moved to the project's [wiki](https://github.com/kosme/arduinoFFT/wiki). diff --git a/changeLog.txt b/changeLog.txt deleted file mode 100644 index d49b854..0000000 --- a/changeLog.txt +++ /dev/null @@ -1,40 +0,0 @@ -02/22/20 v1.9.2 -Fix compilation on AVR systems. - -02/22/20 v1.9.1 -Add setArrays() function because of issue #32. -Add API migration info to README and improve README. -Use better sqrtf() approximation. - -02/19/20 v1.9.0 -Remove deprecated API. Consistent renaming of functions to lowercase. -Make template to be able to use float or double type (float brings a ~70% speed increase on ESP32). -Add option to provide cache for window function weighing factors (~50% speed increase on ESP32). -Add some #defines to enable math approximisations to further speed up code (~40% speed increase on ESP32). - -01/27/20 v1.5.5 -Lookup table for constants c1 and c2 used during FFT comupting. This increases the FFT computing speed in around 5%. - -02/10/18 v1.4 -Transition version. Minor optimization to functions. New API. Deprecation of old functions. - -12/06/18 v1.3 -Add support for mbed development boards. - -09/04/17 v1.2.3 -Finally solves the issue of Arduino IDE not correctly detecting and highlighting the keywords. - -09/03/17 v1.2.2 -Solves a format issue in keywords.txt that prevented keywords from being detected. - -08/28/17 v1.2.1 -Fix to issues 6 and 7. Not cleaning the imaginary vector after each cycle leaded to erroneous calculations and could cause buffer overflows. - -08/04/17 v1.2 -Fix to bug preventing the number of samples to be greater than 128. New logical limit is 32768 samples but it is bound to the RAM on the chip. - -05/12/17 v1.1 -Fix issue that prevented installation through the Arduino Library Manager interface. - -05/11/17 v1.0 -Initial commit to Arduino Library Manager. diff --git a/keywords.txt b/keywords.txt index 3807cdb..49fd943 100644 --- a/keywords.txt +++ b/keywords.txt @@ -17,11 +17,11 @@ FFTWindow KEYWORD1 complexToMagnitude KEYWORD2 compute KEYWORD2 dcRemoval KEYWORD2 -windowing KEYWORD2 -exponent KEYWORD2 -revision KEYWORD2 majorPeak KEYWORD2 +majorPeakParabola KEYWORD2 +revision KEYWORD2 setArrays KEYWORD2 +windowing KEYWORD2 ####################################### # Constants (LITERAL1) @@ -29,13 +29,14 @@ setArrays KEYWORD2 Forward LITERAL1 Reverse LITERAL1 -Rectangle LITERAL1 + +Blackman LITERAL1 +Blackman_Harris LITERAL1 +Blackman_Nuttall LITERAL1 +Flat_top LITERAL1 Hamming LITERAL1 Hann LITERAL1 -Triangle LITERAL1 Nuttall LITERAL1 -Blackman LITERAL1 -Blackman_Nuttall LITERAL1 -Blackman_Harris LITERAL1 -Flat_top LITERAL1 +Rectangle LITERAL1 +Triangle LITERAL1 Welch LITERAL1 diff --git a/library.json b/library.json index 6c35419..de87c29 100644 --- a/library.json +++ b/library.json @@ -25,7 +25,7 @@ "email": "bim.overbohm@googlemail.com" } ], - "version": "1.9.2", + "version": "2.0", "frameworks": ["arduino","mbed","espidf"], "platforms": "*" } diff --git a/library.properties b/library.properties index 0a90947..5826255 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=arduinoFFT -version=1.9.2 +version=2.0 author=Enrique Condes maintainer=Enrique Condes sentence=A library for implementing floating point Fast Fourier Transform calculations on Arduino. diff --git a/src/arduinoFFT.cpp b/src/arduinoFFT.cpp new file mode 100644 index 0000000..1bb5b22 --- /dev/null +++ b/src/arduinoFFT.cpp @@ -0,0 +1,518 @@ +/* + FFT library + Copyright (C) 2010 Didier Longueville + Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (template, speed improvements) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +*/ + +#include "arduinoFFT.h" + +template ArduinoFFT::ArduinoFFT() {} + +template +ArduinoFFT::ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, + T samplingFrequency, bool windowingFactors) + : _samples(samples), _samplingFrequency(samplingFrequency), _vImag(vImag), + _vReal(vReal) { + if (windowingFactors) { + _precompiledWindowingFactors = new T[samples / 2]; + } + _power = exponent(samples); +#ifdef FFT_SPEED_OVER_PRECISION + _oneOverSamples = 1.0 / samples; +#endif +} + +template ArduinoFFT::~ArduinoFFT(void) { + // Destructor + if (_precompiledWindowingFactors) { + delete [] _precompiledWindowingFactors; + } +} + +template void ArduinoFFT::complexToMagnitude(void) const { + complexToMagnitude(this->_vReal, this->_vImag, this->_samples); +} + +template +void ArduinoFFT::complexToMagnitude(T *vReal, T *vImag, + uint_fast16_t samples) const { + // vM is half the size of vReal and vImag + for (uint_fast16_t i = 0; i < samples; i++) { + vReal[i] = sqrt_internal(sq(vReal[i]) + sq(vImag[i])); + } +} + +template void ArduinoFFT::compute(FFTDirection dir) const { + compute(this->_vReal, this->_vImag, this->_samples, exponent(this->_samples), + dir); +} + +template +void ArduinoFFT::compute(T *vReal, T *vImag, uint_fast16_t samples, + FFTDirection dir) const { + compute(vReal, vImag, samples, exponent(samples), dir); +} + +// Computes in-place complex-to-complex FFT +template +void ArduinoFFT::compute(T *vReal, T *vImag, uint_fast16_t samples, + uint_fast8_t power, FFTDirection dir) const { +#ifdef FFT_SPEED_OVER_PRECISION + T oneOverSamples = this->_oneOverSamples; + if (!this->_oneOverSamples) + oneOverSamples = 1.0 / samples; +#endif + // Reverse bits + uint_fast16_t j = 0; + for (uint_fast16_t i = 0; i < (samples - 1); i++) { + if (i < j) { + swap(&vReal[i], &vReal[j]); + if (dir == FFTDirection::Reverse) + swap(&vImag[i], &vImag[j]); + } + uint_fast16_t k = (samples >> 1); + + while (k <= j) { + j -= k; + k >>= 1; + } + j += k; + } + // Compute the FFT + T c1 = -1.0; + T c2 = 0.0; + uint_fast16_t l2 = 1; + for (uint_fast8_t l = 0; (l < power); l++) { + uint_fast16_t l1 = l2; + l2 <<= 1; + T u1 = 1.0; + T u2 = 0.0; + for (j = 0; j < l1; j++) { + for (uint_fast16_t i = j; i < samples; i += l2) { + uint_fast16_t i1 = i + l1; + T t1 = u1 * vReal[i1] - u2 * vImag[i1]; + T t2 = u1 * vImag[i1] + u2 * vReal[i1]; + vReal[i1] = vReal[i] - t1; + vImag[i1] = vImag[i] - t2; + vReal[i] += t1; + vImag[i] += t2; + } + T z = ((u1 * c1) - (u2 * c2)); + u2 = ((u1 * c2) + (u2 * c1)); + u1 = z; + } + +#if defined(__AVR__) && defined(USE_AVR_PROGMEM) + c2 = pgm_read_float_near(&(_c2[l])); + c1 = pgm_read_float_near(&(_c1[l])); +#else + T cTemp = 0.5 * c1; + c2 = sqrt_internal(0.5 - cTemp); + c1 = sqrt_internal(0.5 + cTemp); +#endif + + if (dir == FFTDirection::Forward) { + c2 = -c2; + } + } + // Scaling for reverse transform + if (dir == FFTDirection::Reverse) { + for (uint_fast16_t i = 0; i < samples; i++) { +#ifdef FFT_SPEED_OVER_PRECISION + vReal[i] *= oneOverSamples; + vImag[i] *= oneOverSamples; +#else + vReal[i] /= samples; + vImag[i] /= samples; +#endif + } + } +} + +template void ArduinoFFT::dcRemoval(void) const { + dcRemoval(this->_vReal, this->_samples); +} + +template +void ArduinoFFT::dcRemoval(T *vData, uint_fast16_t samples) const { + // calculate the mean of vData + T mean = 0; + for (uint_fast16_t i = 0; i < samples; i++) { + mean += vData[i]; + } + mean /= samples; + // Subtract the mean from vData + for (uint_fast16_t i = 0; i < samples; i++) { + vData[i] -= mean; + } +} + +template T ArduinoFFT::majorPeak(void) const { + return majorPeak(this->_vReal, this->_samples, this->_samplingFrequency); +} + +template void ArduinoFFT::majorPeak(T *f, T *v) const { + majorPeak(this->_vReal, this->_samples, this->_samplingFrequency, f, v); +} + +template +T ArduinoFFT::majorPeak(T *vData, uint_fast16_t samples, + T samplingFrequency) const { + T frequency; + majorPeak(vData, samples, samplingFrequency, &frequency, nullptr); + return frequency; +} + +template +void ArduinoFFT::majorPeak(T *vData, uint_fast16_t samples, + T samplingFrequency, T *frequency, + T *magnitude) const { + T maxY = 0; + uint_fast16_t IndexOfMaxY = 0; + findMaxY(vData, (samples >> 1) + 1, &maxY, &IndexOfMaxY); + + T delta = 0.5 * ((vData[IndexOfMaxY - 1] - vData[IndexOfMaxY + 1]) / + (vData[IndexOfMaxY - 1] - (2.0 * vData[IndexOfMaxY]) + + vData[IndexOfMaxY + 1])); + T interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples - 1); + if (IndexOfMaxY == (samples >> 1)) // To improve calculation on edge values + interpolatedX = ((IndexOfMaxY + delta) * samplingFrequency) / (samples); + // returned value: interpolated frequency peak apex + *frequency = interpolatedX; + if (magnitude != nullptr) { +#if defined(ESP8266) || defined(ESP32) + *magnitude = fabs(vData[IndexOfMaxY - 1] - (2.0 * vData[IndexOfMaxY]) + + vData[IndexOfMaxY + 1]); +#else + *magnitude = abs(vData[IndexOfMaxY - 1] - (2.0 * vData[IndexOfMaxY]) + + vData[IndexOfMaxY + 1]); +#endif + } +} + +template T ArduinoFFT::majorPeakParabola(void) const { + T freq = 0; + majorPeakParabola(this->_vReal, this->_samples, this->_samplingFrequency, + &freq, nullptr); + return freq; +} + +template +void ArduinoFFT::majorPeakParabola(T *frequency, T *magnitude) const { + majorPeakParabola(this->_vReal, this->_samples, this->_samplingFrequency, + frequency, magnitude); +} + +template +T ArduinoFFT::majorPeakParabola(T *vData, uint_fast16_t samples, + T samplingFrequency) const { + T freq = 0; + majorPeakParabola(vData, samples, samplingFrequency, &freq, nullptr); + return freq; +} + +template +void ArduinoFFT::majorPeakParabola(T *vData, uint_fast16_t samples, + T samplingFrequency, T *frequency, + T *magnitude) const { + T maxY = 0; + uint_fast16_t IndexOfMaxY = 0; + findMaxY(vData, (samples >> 1) + 1, &maxY, &IndexOfMaxY); + + *frequency = 0; + if (IndexOfMaxY > 0) { + // Assume the three points to be on a parabola + T a, b, c; + parabola(IndexOfMaxY - 1, vData[IndexOfMaxY - 1], IndexOfMaxY, + vData[IndexOfMaxY], IndexOfMaxY + 1, vData[IndexOfMaxY + 1], &a, + &b, &c); + + // Peak is at the middle of the parabola + T x = -b / (2 * a); + + // And magnitude is at the extrema of the parabola if you want It... + if (magnitude != nullptr) { + *magnitude = a * x * x + b * x + c; + } + + // Convert to frequency + *frequency = (x * samplingFrequency) / samples; + } +} + +template uint8_t ArduinoFFT::revision(void) { + return (FFT_LIB_REV); +} + +// Replace the data array pointers +template +void ArduinoFFT::setArrays(T *vReal, T *vImag, uint_fast16_t samples) { + _vReal = vReal; + _vImag = vImag; + if (samples) { + _samples = samples; +#ifdef FFT_SPEED_OVER_PRECISION + _oneOverSamples = 1.0 / samples; +#endif + if (_precompiledWindowingFactors) { + delete [] _precompiledWindowingFactors; + } + _precompiledWindowingFactors = new T[samples / 2]; + } +} + +template +void ArduinoFFT::windowing(FFTWindow windowType, FFTDirection dir, + bool withCompensation) { + // The windowing function is the same, precompiled values can be used, and + // precompiled values exist + if (this->_precompiledWindowingFactors && this->_isPrecompiled && + this->_windowFunction == windowType && + this->_precompiledWithCompensation == withCompensation) { + windowing(this->_vReal, this->_samples, FFTWindow::Precompiled, dir, + this->_precompiledWindowingFactors, withCompensation); + // Precompiled values must be generated. Either the function changed or the + // precompiled values don't exist + } else if (this->_precompiledWindowingFactors) { + windowing(this->_vReal, this->_samples, windowType, dir, + this->_precompiledWindowingFactors, withCompensation); + this->_isPrecompiled = true; + this->_precompiledWithCompensation = withCompensation; + this->_windowFunction = windowType; + // Don't care about precompiled windowing values + } else { + windowing(this->_vReal, this->_samples, windowType, dir, nullptr, + withCompensation); + } +} + +template +void ArduinoFFT::windowing(T *vData, uint_fast16_t samples, + FFTWindow windowType, FFTDirection dir, + T *windowingFactors, bool withCompensation) { + // Weighing factors are computed once before multiple use of FFT + // The weighing function is symmetric; half the weighs are recorded + if (windowingFactors != nullptr && windowType == FFTWindow::Precompiled) { + for (uint_fast16_t i = 0; i < (samples >> 1); i++) { + if (dir == FFTDirection::Forward) { + vData[i] *= windowingFactors[i]; + vData[samples - (i + 1)] *= windowingFactors[i]; + } else { +#ifdef FFT_SPEED_OVER_PRECISION + T inverse = 1.0 / windowingFactors[i]; + vData[i] *= inverse; + vData[samples - (i + 1)] *= inverse; +#else + vData[i] /= windowingFactors[i]; + vData[samples - (i + 1)] /= windowingFactors[i]; +#endif + } + } + } else { + T samplesMinusOne = (T(samples) - 1.0); + T compensationFactor; + if (withCompensation) { + compensationFactor = + _WindowCompensationFactors[static_cast(windowType)]; + } + for (uint_fast16_t i = 0; i < (samples >> 1); i++) { + T indexMinusOne = T(i); + T ratio = (indexMinusOne / samplesMinusOne); + T weighingFactor = 1.0; + // Compute and record weighting factor + switch (windowType) { + case FFTWindow::Hamming: // hamming + weighingFactor = 0.54 - (0.46 * cos(twoPi * ratio)); + break; + case FFTWindow::Hann: // hann + weighingFactor = 0.54 * (1.0 - cos(twoPi * ratio)); + break; + case FFTWindow::Triangle: // triangle (Bartlett) +#if defined(ESP8266) || defined(ESP32) + weighingFactor = + 1.0 - ((2.0 * fabs(indexMinusOne - (samplesMinusOne / 2.0))) / + samplesMinusOne); +#else + weighingFactor = + 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / + samplesMinusOne); +#endif + break; + case FFTWindow::Nuttall: // nuttall + weighingFactor = 0.355768 - (0.487396 * (cos(twoPi * ratio))) + + (0.144232 * (cos(fourPi * ratio))) - + (0.012604 * (cos(sixPi * ratio))); + break; + case FFTWindow::Blackman: // blackman + weighingFactor = 0.42323 - (0.49755 * (cos(twoPi * ratio))) + + (0.07922 * (cos(fourPi * ratio))); + break; + case FFTWindow::Blackman_Nuttall: // blackman nuttall + weighingFactor = 0.3635819 - (0.4891775 * (cos(twoPi * ratio))) + + (0.1365995 * (cos(fourPi * ratio))) - + (0.0106411 * (cos(sixPi * ratio))); + break; + case FFTWindow::Blackman_Harris: // blackman harris + weighingFactor = 0.35875 - (0.48829 * (cos(twoPi * ratio))) + + (0.14128 * (cos(fourPi * ratio))) - + (0.01168 * (cos(sixPi * ratio))); + break; + case FFTWindow::Flat_top: // flat top + weighingFactor = 0.2810639 - (0.5208972 * cos(twoPi * ratio)) + + (0.1980399 * cos(fourPi * ratio)); + break; + case FFTWindow::Welch: // welch + weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / + (samplesMinusOne / 2.0)); + break; + default: + // This is Rectangle windowing which doesn't do anything + // and Precompiled which shouldn't be selected + break; + } + if (withCompensation) { + weighingFactor *= compensationFactor; + } + if (windowingFactors) { + windowingFactors[i] = weighingFactor; + } + if (dir == FFTDirection::Forward) { + vData[i] *= weighingFactor; + vData[samples - (i + 1)] *= weighingFactor; + } else { +#ifdef FFT_SPEED_OVER_PRECISION + T inverse = 1.0 / weighingFactor; + vData[i] *= inverse; + vData[samples - (i + 1)] *= inverse; +#else + vData[i] /= weighingFactor; + vData[samples - (i + 1)] /= weighingFactor; +#endif + } + } + } +} + +// Private functions + +template +uint_fast8_t ArduinoFFT::exponent(uint_fast16_t value) const { + // Calculates the base 2 logarithm of a value + uint_fast8_t result = 0; + while (value >>= 1) + result++; + return result; +} + +template +void ArduinoFFT::findMaxY(T *vData, uint_fast16_t length, T *maxY, + uint_fast16_t *index) const { + *maxY = 0; + *index = 0; + // If sampling_frequency = 2 * max_frequency in signal, + // value would be stored at position samples/2 + for (uint_fast16_t i = 1; i < length; i++) { + if ((vData[i - 1] < vData[i]) && (vData[i] > vData[i + 1])) { + if (vData[i] > vData[*index]) { + *index = i; + } + } + } + *maxY = vData[*index]; +} + +template +void ArduinoFFT::parabola(T x1, T y1, T x2, T y2, T x3, T y3, T *a, T *b, + T *c) const { + // const T reversed_denom = 1 / ((x1 - x2) * (x1 - x3) * (x2 - x3)); + // This is a special case in which the three X coordinates are three positive, + // consecutive integers. Therefore the reverse denominator will always be -0.5 + const T reversed_denom = -0.5; + + *a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) * reversed_denom; + *b = (x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1) + x1 * x1 * (y2 - y3)) * + reversed_denom; + *c = (x2 * x3 * (x2 - x3) * y1 + x3 * x1 * (x3 - x1) * y2 + + x1 * x2 * (x1 - x2) * y3) * + reversed_denom; +} + +template void ArduinoFFT::swap(T *a, T *b) const { + T temp = *a; + *a = *b; + *b = temp; +} + +#ifdef FFT_SQRT_APPROXIMATION +// Fast inverse square root aka "Quake 3 fast inverse square root", multiplied +// by x. Uses one iteration of Halley's method for precision. See: +// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Iterative_methods_for_reciprocal_square_roots +// And: https://github.com/HorstBaerbel/approx +template float ArduinoFFT::sqrt_internal(float x) const { + union // get bits for floating point value + { + float x; + int32_t i; + } u; + u.x = x; + u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. + float xu = x * u.x; + float xu2 = xu * u.x; + // Halley's method, repeating increases accuracy + u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); + return u.x; +} + +template double ArduinoFFT::sqrt_internal(double x) const { + // According to HosrtBaerbel, on the ESP32 the approximation is not faster, so + // we use the standard function +#ifdef ESP32 + return sqrt(x); +#else + union // get bits for floating point value + { + double x; + int64_t i; + } u; + u.x = x; + u.i = 0x5fe6ec85e7de30da - (u.i >> 1); // gives initial guess y0. + double xu = x * u.x; + double xu2 = xu * u.x; + // Halley's method, repeating increases accuracy + u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); + return u.x; +#endif +} +#endif + +template +const T ArduinoFFT::_WindowCompensationFactors[10] = { + 1.0000000000 * 2.0, // rectangle (Box car) + 1.8549343278 * 2.0, // hamming + 1.8554726898 * 2.0, // hann + 2.0039186079 * 2.0, // triangle (Bartlett) + 2.8163172034 * 2.0, // nuttall + 2.3673474360 * 2.0, // blackman + 2.7557840395 * 2.0, // blackman nuttall + 2.7929062517 * 2.0, // blackman harris + 3.5659039231 * 2.0, // flat top + 1.5029392863 * 2.0 // welch +}; + +template class ArduinoFFT; +template class ArduinoFFT; diff --git a/src/arduinoFFT.h b/src/arduinoFFT.h index cb968ff..5fb01d0 100644 --- a/src/arduinoFFT.h +++ b/src/arduinoFFT.h @@ -1,22 +1,22 @@ /* - FFT library - Copyright (C) 2010 Didier Longueville - Copyright (C) 2014 Enrique Condes - Copyright (C) 2020 Bim Overbohm (header-only, template, speed improvements) + FFT library + Copyright (C) 2010 Didier Longueville + Copyright (C) 2014 Enrique Condes + Copyright (C) 2020 Bim Overbohm (template, speed improvements) - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see . + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ @@ -29,472 +29,150 @@ #include "WProgram.h" /* This is where the standard Arduino code lies */ #endif #else -#include #include +#include + #ifdef __AVR__ #include #include #endif -#include #include "defs.h" #include "types.h" +#include +#include #endif -// Define this to use reciprocal multiplication for division and some more speedups that might decrease precision -//#define FFT_SPEED_OVER_PRECISION +// This definition uses a low-precision square root approximation instead of the +// regular sqrt() call +// This might only work for specific use cases, but is significantly faster. -// Define this to use a low-precision square root approximation instead of the regular sqrt() call -// This might only work for specific use cases, but is significantly faster. Only works for ArduinoFFT. -//#define FFT_SQRT_APPROXIMATION - -#ifdef FFT_SQRT_APPROXIMATION - #include -#else - #ifndef sqrt_internal - #define sqrt_internal sqrt - #endif +#ifndef FFT_SQRT_APPROXIMATION +#define sqrt_internal sqrt #endif -enum class FFTDirection -{ - Reverse, - Forward -}; +enum class FFTDirection { Forward, Reverse }; -enum class FFTWindow -{ - Rectangle, // rectangle (Box car) - Hamming, // hamming - Hann, // hann - Triangle, // triangle (Bartlett) - Nuttall, // nuttall - Blackman, //blackman - Blackman_Nuttall, // blackman nuttall - Blackman_Harris, // blackman harris - Flat_top, // flat top - Welch // welch +enum class FFTWindow { + Rectangle, // rectangle (Box car) + Hamming, // hamming + Hann, // hann + Triangle, // triangle (Bartlett) + Nuttall, // nuttall + Blackman, // blackman + Blackman_Nuttall, // blackman nuttall + Blackman_Harris, // blackman harris + Flat_top, // flat top + Welch, // welch + Precompiled // Placeholder for using custom or precompiled window values }; +#define FFT_LIB_REV 0x20 +/* Custom constants */ +/* These defines keep compatibility with pre 2.0 code */ +#define FFT_FORWARD FFTDirection::Forward +#define FFT_REVERSE FFTDirection::Reverse -template -class ArduinoFFT -{ +/* Windowing type */ +#define FFT_WIN_TYP_RECTANGLE FFTWindow::Rectangle /* rectangle (Box car) */ +#define FFT_WIN_TYP_HAMMING FFTWindow::Hamming /* hamming */ +#define FFT_WIN_TYP_HANN FFTWindow::Hann /* hann */ +#define FFT_WIN_TYP_TRIANGLE FFTWindow::Triangle /* triangle (Bartlett) */ +#define FFT_WIN_TYP_NUTTALL FFTWindow::Nuttall /* nuttall */ +#define FFT_WIN_TYP_BLACKMAN FFTWindow::Blackman /* blackman */ +#define FFT_WIN_TYP_BLACKMAN_NUTTALL \ + FFTWindow::Blackman_Nuttall /* blackman nuttall */ +#define FFT_WIN_TYP_BLACKMAN_HARRIS \ + FFTWindow::Blackman_Harris /* blackman harris*/ +#define FFT_WIN_TYP_FLT_TOP FFTWindow::Flat_top /* flat top */ +#define FFT_WIN_TYP_WELCH FFTWindow::Welch /* welch */ +/* End of compatibility defines */ + +/* Mathematial constants */ +#define twoPi 6.28318531 +#define fourPi 12.56637061 +#define sixPi 18.84955593 + +template class ArduinoFFT { public: - // Constructor - ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, T *windowWeighingFactors = nullptr) - : _vReal(vReal) - , _vImag(vImag) - , _samples(samples) -#ifdef FFT_SPEED_OVER_PRECISION - , _oneOverSamples(1.0 / samples) -#endif - , _samplingFrequency(samplingFrequency) - , _windowWeighingFactors(windowWeighingFactors) - { - // Calculates the base 2 logarithm of sample count - _power = 0; - while (((samples >> _power) & 1) != 1) - { - _power++; - } - } + ArduinoFFT(); + ArduinoFFT(T *vReal, T *vImag, uint_fast16_t samples, T samplingFrequency, + bool windowingFactors = false); - // Destructor - ~ArduinoFFT() - { - } + ~ArduinoFFT(); - // Get library revision - static uint8_t revision() - { - return 0x19; - } + void complexToMagnitude(void) const; + void complexToMagnitude(T *vReal, T *vImag, uint_fast16_t samples) const; - // Replace the data array pointers - void setArrays(T *vReal, T *vImag) - { - _vReal = vReal; - _vImag = vImag; - } + void compute(FFTDirection dir) const; + void compute(T *vReal, T *vImag, uint_fast16_t samples, + FFTDirection dir) const; + void compute(T *vReal, T *vImag, uint_fast16_t samples, uint_fast8_t power, + FFTDirection dir) const; - // Computes in-place complex-to-complex FFT - void compute(FFTDirection dir) const - { - // Reverse bits / - uint_fast16_t j = 0; - for (uint_fast16_t i = 0; i < (this->_samples - 1); i++) - { - if (i < j) - { - Swap(this->_vReal[i], this->_vReal[j]); - if (dir == FFTDirection::Reverse) - { - Swap(this->_vImag[i], this->_vImag[j]); - } - } - uint_fast16_t k = (this->_samples >> 1); - while (k <= j) - { - j -= k; - k >>= 1; - } - j += k; - } - // Compute the FFT -#ifdef __AVR__ - uint_fast8_t index = 0; -#endif - T c1 = -1.0; - T c2 = 0.0; - uint_fast16_t l2 = 1; - for (uint_fast8_t l = 0; (l < this->_power); l++) - { - uint_fast16_t l1 = l2; - l2 <<= 1; - T u1 = 1.0; - T u2 = 0.0; - for (j = 0; j < l1; j++) - { - for (uint_fast16_t i = j; i < this->_samples; i += l2) - { - uint_fast16_t i1 = i + l1; - T t1 = u1 * this->_vReal[i1] - u2 * this->_vImag[i1]; - T t2 = u1 * this->_vImag[i1] + u2 * this->_vReal[i1]; - this->_vReal[i1] = this->_vReal[i] - t1; - this->_vImag[i1] = this->_vImag[i] - t2; - this->_vReal[i] += t1; - this->_vImag[i] += t2; - } - T z = ((u1 * c1) - (u2 * c2)); - u2 = ((u1 * c2) + (u2 * c1)); - u1 = z; - } -#ifdef __AVR__ - c2 = pgm_read_float_near(&(_c2[index])); - c1 = pgm_read_float_near(&(_c1[index])); - index++; -#else - T cTemp = 0.5 * c1; - c2 = sqrt_internal(0.5 - cTemp); - c1 = sqrt_internal(0.5 + cTemp); -#endif - c2 = dir == FFTDirection::Forward ? -c2 : c2; - } - // Scaling for reverse transform - if (dir != FFTDirection::Forward) - { - for (uint_fast16_t i = 0; i < this->_samples; i++) - { -#ifdef FFT_SPEED_OVER_PRECISION - this->_vReal[i] *= _oneOverSamples; - this->_vImag[i] *= _oneOverSamples; -#else - this->_vReal[i] /= this->_samples; - this->_vImag[i] /= this->_samples; -#endif - } - } - } + void dcRemoval(void) const; + void dcRemoval(T *vData, uint_fast16_t samples) const; - void complexToMagnitude() const - { - // vM is half the size of vReal and vImag - for (uint_fast16_t i = 0; i < this->_samples; i++) - { - this->_vReal[i] = sqrt_internal(sq(this->_vReal[i]) + sq(this->_vImag[i])); - } - } + T majorPeak(void) const; + void majorPeak(T *f, T *v) const; + T majorPeak(T *vData, uint_fast16_t samples, T samplingFrequency) const; + void majorPeak(T *vData, uint_fast16_t samples, T samplingFrequency, + T *frequency, T *magnitude) const; - void dcRemoval() const - { - // calculate the mean of vData - T mean = 0; - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - mean += this->_vReal[i]; - } - mean /= this->_samples; - // Subtract the mean from vData - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - this->_vReal[i] -= mean; - } - } + T majorPeakParabola(void) const; + void majorPeakParabola(T *frequency, T *magnitude) const; + T majorPeakParabola(T *vData, uint_fast16_t samples, + T samplingFrequency) const; + void majorPeakParabola(T *vData, uint_fast16_t samples, T samplingFrequency, + T *frequency, T *magnitude) const; - void windowing(FFTWindow windowType, FFTDirection dir, bool withCompensation = false) - { - // check if values are already pre-computed for the correct window type and compensation - if (_windowWeighingFactors && _weighingFactorsComputed && - _weighingFactorsFFTWindow == windowType && - _weighingFactorsWithCompensation == withCompensation) - { - // yes. values are precomputed - if (dir == FFTDirection::Forward) - { - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { - this->_vReal[i] *= _windowWeighingFactors[i]; - this->_vReal[this->_samples - (i + 1)] *= _windowWeighingFactors[i]; - } - } - else - { - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { -#ifdef FFT_SPEED_OVER_PRECISION - // on many architectures reciprocals and multiplying are much faster than division - T oneOverFactor = 1.0 / _windowWeighingFactors[i]; - this->_vReal[i] *= oneOverFactor; - this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; -#else - this->_vReal[i] /= _windowWeighingFactors[i]; - this->_vReal[this->_samples - (i + 1)] /= _windowWeighingFactors[i]; -#endif - } - } - } - else - { - // no. values need to be pre-computed or applied - T samplesMinusOne = (T(this->_samples) - 1.0); - T compensationFactor = _WindowCompensationFactors[static_cast(windowType)]; - for (uint_fast16_t i = 0; i < (this->_samples >> 1); i++) - { - T indexMinusOne = T(i); - T ratio = (indexMinusOne / samplesMinusOne); - T weighingFactor = 1.0; - // Compute and record weighting factor - switch (windowType) - { - case FFTWindow::Rectangle: // rectangle (box car) - weighingFactor = 1.0; - break; - case FFTWindow::Hamming: // hamming - weighingFactor = 0.54 - (0.46 * cos(TWO_PI * ratio)); - break; - case FFTWindow::Hann: // hann - weighingFactor = 0.54 * (1.0 - cos(TWO_PI * ratio)); - break; - case FFTWindow::Triangle: // triangle (Bartlett) - weighingFactor = 1.0 - ((2.0 * abs(indexMinusOne - (samplesMinusOne / 2.0))) / samplesMinusOne); - break; - case FFTWindow::Nuttall: // nuttall - weighingFactor = 0.355768 - (0.487396 * (cos(TWO_PI * ratio))) + (0.144232 * (cos(FOUR_PI * ratio))) - (0.012604 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Blackman: // blackman - weighingFactor = 0.42323 - (0.49755 * (cos(TWO_PI * ratio))) + (0.07922 * (cos(FOUR_PI * ratio))); - break; - case FFTWindow::Blackman_Nuttall: // blackman nuttall - weighingFactor = 0.3635819 - (0.4891775 * (cos(TWO_PI * ratio))) + (0.1365995 * (cos(FOUR_PI * ratio))) - (0.0106411 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Blackman_Harris: // blackman harris - weighingFactor = 0.35875 - (0.48829 * (cos(TWO_PI * ratio))) + (0.14128 * (cos(FOUR_PI * ratio))) - (0.01168 * (cos(SIX_PI * ratio))); - break; - case FFTWindow::Flat_top: // flat top - weighingFactor = 0.2810639 - (0.5208972 * cos(TWO_PI * ratio)) + (0.1980399 * cos(FOUR_PI * ratio)); - break; - case FFTWindow::Welch: // welch - weighingFactor = 1.0 - sq((indexMinusOne - samplesMinusOne / 2.0) / (samplesMinusOne / 2.0)); - break; - } - if (withCompensation) - { - weighingFactor *= compensationFactor; - } - if (_windowWeighingFactors) - { - _windowWeighingFactors[i] = weighingFactor; - } - if (dir == FFTDirection::Forward) - { - this->_vReal[i] *= weighingFactor; - this->_vReal[this->_samples - (i + 1)] *= weighingFactor; - } - else - { -#ifdef FFT_SPEED_OVER_PRECISION - // on many architectures reciprocals and multiplying are much faster than division - T oneOverFactor = 1.0 / weighingFactor; - this->_vReal[i] *= oneOverFactor; - this->_vReal[this->_samples - (i + 1)] *= oneOverFactor; -#else - this->_vReal[i] /= weighingFactor; - this->_vReal[this->_samples - (i + 1)] /= weighingFactor; -#endif - } - } - // mark cached values as pre-computed - _weighingFactorsFFTWindow = windowType; - _weighingFactorsWithCompensation = withCompensation; - _weighingFactorsComputed = true; - } - } + uint8_t revision(void); - T majorPeak() const - { - T maxY = 0; - uint_fast16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) - { - if (this->_vReal[i] > maxY) - { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); - T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) - { - //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - } - // returned value: interpolated frequency peak apex - return interpolatedX; - } + void setArrays(T *vReal, T *vImag, uint_fast16_t samples = 0); - void majorPeak(T &frequency, T &value) const - { - T maxY = 0; - uint_fast16_t IndexOfMaxY = 0; - //If sampling_frequency = 2 * max_frequency in signal, - //value would be stored at position samples/2 - for (uint_fast16_t i = 1; i < ((this->_samples >> 1) + 1); i++) - { - if ((this->_vReal[i - 1] < this->_vReal[i]) && (this->_vReal[i] > this->_vReal[i + 1])) - { - if (this->_vReal[i] > maxY) - { - maxY = this->_vReal[i]; - IndexOfMaxY = i; - } - } - } - T delta = 0.5 * ((this->_vReal[IndexOfMaxY - 1] - this->_vReal[IndexOfMaxY + 1]) / (this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1])); - T interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples - 1); - if (IndexOfMaxY == (this->_samples >> 1)) - { - //To improve calculation on edge values - interpolatedX = ((IndexOfMaxY + delta) * this->_samplingFrequency) / (this->_samples); - } - // returned value: interpolated frequency peak apex - frequency = interpolatedX; - value = abs(this->_vReal[IndexOfMaxY - 1] - (2.0 * this->_vReal[IndexOfMaxY]) + this->_vReal[IndexOfMaxY + 1]); - } + void windowing(FFTWindow windowType, FFTDirection dir, + bool withCompensation = false); + void windowing(T *vData, uint_fast16_t samples, FFTWindow windowType, + FFTDirection dir, T *windowingFactors = nullptr, + bool withCompensation = false); private: -#ifdef __AVR__ - static const float _c1[] PROGMEM; - static const float _c2[] PROGMEM; + /* Variables */ + static const T _WindowCompensationFactors[10]; +#ifdef FFT_SPEED_OVER_PRECISION + T _oneOverSamples = 0.0; #endif - static const T _WindowCompensationFactors[10]; - - // Mathematial constants -#ifndef TWO_PI - static constexpr T TWO_PI = 6.28318531; // might already be defined in Arduino.h -#endif - static constexpr T FOUR_PI = 12.56637061; - static constexpr T SIX_PI = 18.84955593; - - static inline void Swap(T &x, T &y) - { - T temp = x; - x = y; - y = temp; - } + bool _isPrecompiled = false; + bool _precompiledWithCompensation = false; + uint_fast8_t _power = 0; + T *_precompiledWindowingFactors; + uint_fast16_t _samples; + T _samplingFrequency; + T *_vImag; + T *_vReal; + FFTWindow _windowFunction; + /* Functions */ + uint_fast8_t exponent(uint_fast16_t value) const; + void findMaxY(T *vData, uint_fast16_t length, T *maxY, + uint_fast16_t *index) const; + void parabola(T x1, T y1, T x2, T y2, T x3, T y3, T *a, T *b, T *c) const; + void swap(T *a, T *b) const; #ifdef FFT_SQRT_APPROXIMATION - // Fast inverse square root aka "Quake 3 fast inverse square root", multiplied by x. - // Uses one iteration of Halley's method for precision. - // See: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Iterative_methods_for_reciprocal_square_roots - // And: https://github.com/HorstBaerbel/approx - template - static inline V sqrt_internal(typename std::enable_if::value, V>::type x) - { - union // get bits for float value - { - float x; - int32_t i; - } u; - u.x = x; - u.i = 0x5f375a86 - (u.i >> 1); // gives initial guess y0. - float xu = x * u.x; - float xu2 = xu * u.x; - u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy - return u.x; - } - - template - static inline V sqrt_internal(typename std::enable_if::value, V>::type x) - { - // According to HosrtBaerbel, on the ESP32 the approximation is not faster, so we use the standard function - #ifdef ESP32 - return sqrt(x); - #else - union // get bits for float value - { - double x; - int64_t i; - } u; - u.x = x; - u.i = 0x5fe6ec85e7de30da - (u.i >> 1); // gives initial guess y0. - double xu = x * u.x; - double xu2 = xu * u.x; - u.x = (0.125 * 3.0) * xu * (5.0 - xu2 * ((10.0 / 3.0) - xu2)); // Halley's method, repeating increases accuracy - return u.x; - #endif - } + float sqrt_internal(float x) const; + double sqrt_internal(double x) const; #endif - - /* Variables */ - T *_vReal = nullptr; - T *_vImag = nullptr; - uint_fast16_t _samples = 0; -#ifdef FFT_SPEED_OVER_PRECISION - T _oneOverSamples = 0.0; -#endif - T _samplingFrequency = 0; - T *_windowWeighingFactors = nullptr; - FFTWindow _weighingFactorsFFTWindow; - bool _weighingFactorsWithCompensation = false; - bool _weighingFactorsComputed = false; - uint_fast8_t _power = 0; }; -#ifdef __AVR__ -template -const float ArduinoFFT::_c1[] PROGMEM = { - 0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, - 0.9951847267, 0.9987954562, 0.9996988187, 0.9999247018, - 0.9999811753, 0.9999952938, 0.9999988235, 0.9999997059, - 0.9999999265, 0.9999999816, 0.9999999954, 0.9999999989, - 0.9999999997}; - -template -const float ArduinoFFT::_c2[] PROGMEM = { - 1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, - 0.0980171403, 0.0490676743, 0.0245412285, 0.0122715383, - 0.0061358846, 0.0030679568, 0.0015339802, 0.0007669903, - 0.0003834952, 0.0001917476, 0.0000958738, 0.0000479369, - 0.0000239684}; +#if defined(__AVR__) && defined(USE_AVR_PROGMEM) +static const float _c1[] PROGMEM = { + 0.0000000000, 0.7071067812, 0.9238795325, 0.9807852804, 0.9951847267, + 0.9987954562, 0.9996988187, 0.9999247018, 0.9999811753, 0.9999952938, + 0.9999988235, 0.9999997059, 0.9999999265, 0.9999999816, 0.9999999954, + 0.9999999989, 0.9999999997}; +static const float _c2[] PROGMEM = { + 1.0000000000, 0.7071067812, 0.3826834324, 0.1950903220, 0.0980171403, + 0.0490676743, 0.0245412285, 0.0122715383, 0.0061358846, 0.0030679568, + 0.0015339802, 0.0007669903, 0.0003834952, 0.0001917476, 0.0000958738, + 0.0000479369, 0.0000239684}; #endif -template -const T ArduinoFFT::_WindowCompensationFactors[10] = { - 1.0000000000 * 2.0, // rectangle (Box car) - 1.8549343278 * 2.0, // hamming - 1.8554726898 * 2.0, // hann - 2.0039186079 * 2.0, // triangle (Bartlett) - 2.8163172034 * 2.0, // nuttall - 2.3673474360 * 2.0, // blackman - 2.7557840395 * 2.0, // blackman nuttall - 2.7929062517 * 2.0, // blackman harris - 3.5659039231 * 2.0, // flat top - 1.5029392863 * 2.0 // welch -}; - #endif