From 784adf7c16558ae7383cf770290d9f702d52fa30 Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Tue, 28 Feb 2023 11:50:14 +0100 Subject: [PATCH] removed all fft features for simplicity They weren't really needed. They just created confusion. --- .github/workflows/rust.yml | 36 ++------ CHANGELOG.md | 8 +- Cargo.toml | 16 +--- EDUCATIONAL.md | 18 ++-- README.md | 27 +++--- check-build.sh | 22 +---- shell.nix | 1 + src/{fft/microfft_real.rs => fft.rs} | 47 +++++------ src/fft/microfft_complex.rs | 121 --------------------------- src/fft/mod.rs | 100 ---------------------- src/fft/rustfft_complex.rs | 84 ------------------- src/lib.rs | 27 ++---- 12 files changed, 65 insertions(+), 442 deletions(-) rename src/{fft/microfft_real.rs => fft.rs} (75%) delete mode 100644 src/fft/microfft_complex.rs delete mode 100644 src/fft/mod.rs delete mode 100644 src/fft/rustfft_complex.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87efede..457f536 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,29 +29,19 @@ jobs: # build with all features/fft implementations - run: cargo build --all-targets - - run: cargo build --all-targets --no-default-features --features "rustfft-complex" - - run: cargo build --all-targets --no-default-features --features "microfft-complex" - - run: cargo build --all-targets --no-default-features --features "microfft-real" # run tests with all features/fft implementations - run: cargo test --all-targets - - run: cargo test --all-targets --no-default-features --features "rustfft-complex" - - run: cargo test --all-targets --no-default-features --features "microfft-complex" - - run: cargo test --all-targets --no-default-features --features "microfft-real" # run benchmark: right now, there is no reporting or so from the results - run: cargo bench # test `no_std`-build with all features/fft implementations - run: rustup target add thumbv7em-none-eabihf - - run: cargo check --target thumbv7em-none-eabihf --no-default-features --features "microfft-complex" - - run: cargo check --target thumbv7em-none-eabihf --no-default-features --features "microfft-real" + - run: cargo build --target thumbv7em-none-eabihf # run examples with all features/fft implementations - run: cargo run --release --example mp3-samples - - run: cargo run --release --example mp3-samples --no-default-features --features "rustfft-complex" - - run: cargo run --release --example mp3-samples --no-default-features --features "microfft-complex" - - run: cargo run --release --example mp3-samples --no-default-features --features "microfft-real" style_checks: runs-on: ubuntu-latest @@ -68,21 +58,11 @@ jobs: profile: default toolchain: ${{ matrix.rust }} override: true - - name: Rustfmt (checks all source code/all features) + - name: Install required Linux packages for "audio-visualizer"/cpal/minifb + run: sudo apt update && sudo apt -y install libasound2-dev libxkbcommon-dev + - name: Rustfmt run: cargo fmt -- --check - - name: Clippy (default feature) - run: cargo clippy - - name: Clippy (rustfft-complex) - run: cargo clippy --no-default-features --features "rustfft-complex" - - name: Clippy (microfft-complex) - run: cargo clippy --no-default-features --features "microfft-complex" - - name: Clippy (microfft-real) - run: cargo clippy --no-default-features --features "microfft-real" - - name: Rustdoc (default feature) - run: cargo doc - - name: Rustdoc (rustfft-complex) - run: cargo doc --no-default-features --features "rustfft-complex" - - name: Rustdoc (microfft-complex) - run: cargo doc --no-default-features --features "microfft-complex" - - name: Rustdoc (microfft-real) - run: cargo doc --no-default-features --features "microfft-real" + - name: Clippy + run: cargo clippy --all-targets + - name: Rustdoc + run: cargo doc --document-private-items diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ed734..08a4ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog -## 1.3.0 (2022-12-XX) +# 1.4.0 (2023-03-04) +- dropped all optional FFT features (`microfft-complex`, `microfft-real`, + `rustfft-complex`) and made `microfft::real` the default FFT implementation. + This is breaking but only for a small percentage of users. - dependency updates + +## 1.3.0 (2023-03-04) - MSRV is now `1.61.0` - `FrequencySpectrum::apply_scaling_fn` now requires a reference to `&mut self`: This is breaking but only for a small percentage of users. Performance is @@ -12,7 +17,6 @@ This is breaking but only for a small percentage of users. - `FrequencySpectrum::to_mel_map` added for getting the spectrum in the [mel](https://en.wikipedia.org/wiki/Mel_scale) scale. -- - small internal code quality and performance improvements ## 1.2.6 (2022-07-20) diff --git a/Cargo.toml b/Cargo.toml index 736f858..6caf87a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,22 +24,8 @@ exclude = [ name = "fft_spectrum_bench" harness = false -[features] -# by default we use microfft-real: it's the fastest and totally fit's our needs. -# As of version 0.5.0 there is no advantage by using other implementations. -# They still exist mainly for educational purposes, testing during development -# and for possible future enhancements -default = ["microfft-real"] -# std -rustfft-complex = ["rustfft"] -# no_std -microfft-complex = ["microfft"] -# no_std, overall fastest -microfft-real = ["microfft"] - [dependencies] -rustfft = { version = "6.0", optional = true } -microfft = { version = "0.5", optional = true, features = ["size-16384"] } +microfft = { version = "0.5", features = ["size-16384"] } # approx. compare floats; not only in tests but also during runtime float-cmp = "0.9" # sin() cos() log10() etc for no_std-environments; these are not part of Core library diff --git a/EDUCATIONAL.md b/EDUCATIONAL.md index e247a73..9b68ea0 100644 --- a/EDUCATIONAL.md +++ b/EDUCATIONAL.md @@ -1,11 +1,11 @@ ## How to use FFT to get a frequency spectrum? -This library is full of additional and useful links and comments about how an FFT result -can be used to get a frequency spectrum. In this document I want to give a short introduction +This library is full of additional and useful links and comments about how an FFT result +can be used to get a frequency spectrum. In this document I want to give a short introduction where inside the code you can find specific things. -**TL;DR:** Although this crate has 1400 lines of code, **the part which gets the frequency and -their values from the FFT is small and simple**. Most of the code is related to my convenient +**TL;DR:** Although this crate has over 1000 lines of code, **the part which gets the frequency and +their values from the FFT is small and simple**. Most of the code is related to my convenient abstraction over the FFT result including several getters, transform/scaling functions, and tests. @@ -13,11 +13,11 @@ tests. If you want to understand that too: - check out all links provided [at the end of README.md](/README.md) -- look into `lib.rs` (**probalby gives you 90 percent of the things you want to know**) - and the comments over the FFT abstraction in `src/fft/mod.rs` and +- look into `lib.rs` (**probalby gives you 90 percent of the things you want to know**) + and the comments over the FFT abstraction in `src/fft/mod.rs` and `src/fft/rustfft-complex/mod.rs`. - -This is everything important you need. Everything inside - `spectrum.rs` and the other files is just convenient stuff + tests for when you + +This is everything important you need. Everything inside + `spectrum.rs` and the other files is just convenient stuff + tests for when you want to use this crate in your program. diff --git a/README.md b/README.md index 6e6d211..cbd9856 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,13 @@ mainly for educational reasons and to support me during programming/testing. # by default feature "microfft-real" is used [dependencies] spectrum-analyzer = "" - -# or if you need another feature (FFT implementation) -[dependencies.spectrum-analyzer] -default-features = false # important! only one feature at a time works! -version = "" -features = ["rustfft-complex"] # or on of the other features ``` ### your_binary.rs ```rust use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit}; use spectrum_analyzer::windows::hann_window; -use spectrum_analyzer::scaling::divide_by_N; +use spectrum_analyzer::scaling::divide_by_N_sqrt; /// Minimal example. fn main() { @@ -52,7 +46,7 @@ fn main() { // optional frequency limit: e.g. only interested in frequencies 50 <= f <= 150? FrequencyLimit::All, // optional scale - Some(÷_by_N), + Some(÷_by_N_sqrt), ).unwrap(); for (fr, fr_val) in spectrum_hann_window.data().iter() { @@ -64,13 +58,16 @@ fn main() { ## Performance *Measurements taken on i7-1165G7 @ 2.80GHz (Single-threaded) with optimized build* -| Operation | Time | -| ------------------------------------------------------ | ------:| -| Hann Window with 4096 samples | ≈68µs | -| Hamming Window with 4096 samples | ≈118µs | -| FFT (`rustfft/complex`) to spectrum with 4096 samples | ≈170µs | -| FFT (`microfft/real`) to spectrum with 4096 samples | ≈90µs | -| FFT (`microfft/complex`) to spectrum with 4096 samples | ≈250µs | +I've tested multiple FFT implementations. Below you can find out why I decided +to use `microfft`. + +| Operation | Time | +|---------------------------------------------------------| ------:| +| Hann Window with 4096 samples | ≈68µs | +| Hamming Window with 4096 samples | ≈118µs | +| FFT (`rustfft`) to spectrum with 4096 samples | ≈170µs | +| FFT (`microfft::real`) to spectrum with 4096 samples | ≈90µs | +| FFT (`microfft::complex`) to spectrum with 4096 samples | ≈250µs | ## Example Visualizations In the following examples you can see a basic visualization of the spectrum from `0 to 4000Hz` for diff --git a/check-build.sh b/check-build.sh index b16eb2d..9435bbc 100755 --- a/check-build.sh +++ b/check-build.sh @@ -3,38 +3,20 @@ set -x echo "checks that this builds on std+no_std + that all tests run + that all features compile" cargo build --all-targets -cargo build --all-targets --no-default-features --features "rustfft-complex" -cargo build --all-targets --no-default-features --features "microfft-complex" -cargo build --all-targets --no-default-features --features "microfft-real" cargo test --all-targets -cargo test --all-targets --no-default-features --features "rustfft-complex" -cargo test --all-targets --no-default-features --features "microfft-complex" -cargo test --all-targets --no-default-features --features "microfft-real" cargo bench cargo fmt -- --check # (--check doesn't change the files) -cargo doc -cargo doc --no-default-features --features "rustfft-complex" -cargo doc --no-default-features --features "microfft-complex" -cargo doc --no-default-features --features "microfft-real" +cargo doc --document-private-items cargo clippy --all-targets -cargo clippy --all-targets --no-default-features --features "rustfft-complex" -cargo clippy --all-targets --no-default-features --features "microfft-complex" -cargo clippy --all-targets --no-default-features --features "microfft-real" # test no_std rustup target add thumbv7em-none-eabihf -# nope, thats BS: this crate needs STD -# cargo check --target thumbv7em-none-eabihf --no-default-features --features "rustfft-complex" -cargo check --target thumbv7em-none-eabihf --no-default-features --features "microfft-complex" -cargo check --target thumbv7em-none-eabihf --no-default-features --features "microfft-real" +cargo build --target thumbv7em-none-eabihf # run examples cargo run --release --example mp3-samples -cargo run --release --example mp3-samples --no-default-features --features "rustfft-complex" -cargo run --release --example mp3-samples --no-default-features --features "microfft-complex" -cargo run --release --example mp3-samples --no-default-features --features "microfft-real" diff --git a/shell.nix b/shell.nix index 6878552..59bf4be 100644 --- a/shell.nix +++ b/shell.nix @@ -2,6 +2,7 @@ pkgs.mkShell rec { nativeBuildInputs = with pkgs; [ pkg-config + cargo-nextest ]; buildInputs = with pkgs; [ diff --git a/src/fft/microfft_real.rs b/src/fft.rs similarity index 75% rename from src/fft/microfft_real.rs rename to src/fft.rs index b933d5d..745f872 100644 --- a/src/fft/microfft_real.rs +++ b/src/fft.rs @@ -21,13 +21,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -//! Real FFT using `microfft::real`. -//! Works in `no_std`-environments, maximum sample length is 4096 (with microfft version 0.4.0) -//! and it's faster than a "typical" complex FFT. -use alloc::vec::Vec; +//! Real FFT using [`microfft::real`] that is very fast and also works in `no_std` +//! environments. It is faster than regular fft (with the `rustfft` crate for +//! example). The difference to a complex FFT, as with `rustfft` is, that the +//! result vector contains less results as there are no mirrored frequencies. -use crate::fft::Fft; +use alloc::vec::Vec; use core::convert::TryInto; use microfft::real; @@ -36,13 +36,21 @@ use microfft::real; /// it's own version that gets used in lib.rs for binary compatibility. pub use microfft::Complex32; -/// Dummy struct with no properties but used as a type -/// to implement a concrete FFT strategy using (`microfft::real`). +/// Real FFT using [`microfft::real`]. pub struct FftImpl; -impl Fft for FftImpl { +impl FftImpl { + /// Calculates the FFT For the given input samples and returns a Vector of + /// of [`Complex32`] with length `samples.len() / 2 + 1`, where the first + /// index corresponds to the DC component and the last index to the Nyquist + /// frequency. + /// + /// # Parameters + /// - `samples`: Array with samples. Each value must be a regular floating + /// point number (no NaN or infinite) and the length must be + /// a power of two. Otherwise, the function panics. #[inline] - fn fft_apply(samples: &[f32]) -> Vec { + pub(crate) fn calc(samples: &[f32]) -> Vec { let buffer = samples; let mut res = { if buffer.len() == 2 { @@ -94,29 +102,12 @@ impl Fft for FftImpl { } }; - // `microfft::real` documentation says: the Nyquist frequency real value is - // packed inside the imaginary part of the DC component. + // `microfft::real` documentation says: the Nyquist frequency real value + // is packed inside the imaginary part of the DC component. let nyquist_fr_pos_val = res[0].im; res[0].im = 0.0; // manually add the nyquist frequency res.push(Complex32::new(nyquist_fr_pos_val, 0.0)); res } - - #[inline] - fn fft_relevant_res_samples_count(samples_len: usize) -> usize { - // `microfft::real` uses a real FFT and the result is exactly - // N/2 elements of type Complex long. The documentation of - // `microfft::real` says about this: - // The produced output is the first half out the output returned by - // the corresponding `N`-point CFFT, i.e. the real DC value and - // `N/2 - 1` positive-frequency terms. Additionally, the real-valued - // coefficient at the Nyquist frequency is packed into the imaginary part - // of the DC bin. - // - // But as you can see in apply_fft() I manually add the Nyquist frequency - // therefore "+1". For this real-FFT implementation this equals to the total - // length of fft_apply()-result - samples_len / 2 + 1 - } } diff --git a/src/fft/microfft_complex.rs b/src/fft/microfft_complex.rs deleted file mode 100644 index 0f69de1..0000000 --- a/src/fft/microfft_complex.rs +++ /dev/null @@ -1,121 +0,0 @@ -/* -MIT License - -Copyright (c) 2023 Philipp Schuster - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -//! Complex FFT using `microfft::complex`. Results should be equal to the ones from `rustfft`. -//! The difference is that this implementation works in `no_std`-environments but it is -//! limited to a maximum sample length of 16384 (with microfft version 0.5.0) - -use alloc::vec::Vec; - -use crate::fft::Fft; -use core::convert::TryInto; -use microfft::complex; - -/// The result of a FFT is always complex but because different FFT crates might -/// use different versions of "num-complex", each implementation exports -/// it's own version that gets used in lib.rs for binary compatibility. -pub use microfft::Complex32; - -/// Dummy struct with no properties but used as a type -/// to implement a concrete FFT strategy using (`microfft::complex`). -pub struct FftImpl; - -impl FftImpl { - /// Converts all samples to a complex number (imaginary part is set to zero) - /// as preparation for the FFT. - /// - /// ## Parameters - /// `samples` Input samples. - /// - /// ## Return value - /// New vector with elements of FFT output/result. - #[inline] - fn samples_to_complex(samples: &[f32]) -> Vec { - samples - .iter() - .map(|x| Complex32::new(*x, 0.0)) - .collect::>() - } -} - -impl Fft for FftImpl { - #[inline] - fn fft_apply(samples: &[f32]) -> Vec { - let buffer = Self::samples_to_complex(samples); - - if buffer.len() == 2 { - let mut buffer: [_; 2] = buffer.try_into().unwrap(); - complex::cfft_2(&mut buffer).to_vec() - } else if buffer.len() == 4 { - let mut buffer: [_; 4] = buffer.try_into().unwrap(); - complex::cfft_4(&mut buffer).to_vec() - } else if buffer.len() == 8 { - let mut buffer: [_; 8] = buffer.try_into().unwrap(); - complex::cfft_8(&mut buffer).to_vec() - } else if buffer.len() == 16 { - let mut buffer: [_; 16] = buffer.try_into().unwrap(); - complex::cfft_16(&mut buffer).to_vec() - } else if buffer.len() == 32 { - let mut buffer: [_; 32] = buffer.try_into().unwrap(); - complex::cfft_32(&mut buffer).to_vec() - } else if buffer.len() == 64 { - let mut buffer: [_; 64] = buffer.try_into().unwrap(); - complex::cfft_64(&mut buffer).to_vec() - } else if buffer.len() == 128 { - let mut buffer: [_; 128] = buffer.try_into().unwrap(); - complex::cfft_128(&mut buffer).to_vec() - } else if buffer.len() == 256 { - let mut buffer: [_; 256] = buffer.try_into().unwrap(); - complex::cfft_256(&mut buffer).to_vec() - } else if buffer.len() == 512 { - let mut buffer: [_; 512] = buffer.try_into().unwrap(); - complex::cfft_512(&mut buffer).to_vec() - } else if buffer.len() == 1024 { - let mut buffer: [_; 1024] = buffer.try_into().unwrap(); - complex::cfft_1024(&mut buffer).to_vec() - } else if buffer.len() == 2048 { - let mut buffer: [_; 2048] = buffer.try_into().unwrap(); - complex::cfft_2048(&mut buffer).to_vec() - } else if buffer.len() == 4096 { - let mut buffer: [_; 4096] = buffer.try_into().unwrap(); - complex::cfft_4096(&mut buffer).to_vec() - } else { - panic!("`microfft::complex` only supports powers of 2 between 2 and 4096!"); - } - } - - #[inline] - fn fft_relevant_res_samples_count(samples_len: usize) -> usize { - // See https://stackoverflow.com/a/4371627/2891595 for more information as well as - // https://www.gaussianwaves.com/2015/11/interpreting-fft-results-complex-dft-frequency-bins-and-fftshift/ - // - // The indices 0 to N/2 (inclusive) are usually the most relevant. Although, index - // N/2-1 is declared as the last useful one on stackoverflow (because in typical applications - // Nyquist-frequency + above are filtered out), we include everything here. - // with 0..=(samples_len / 2) (inclusive) we get all frequencies from 0 to Nyquist theorem. - // - // Indices (samples_len / 2)..len() are mirrored/negative. You can also see this here: - // https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2015/11/realDFT_complexDFT.png - samples_len / 2 + 1 - } -} diff --git a/src/fft/mod.rs b/src/fft/mod.rs deleted file mode 100644 index 06c0849..0000000 --- a/src/fft/mod.rs +++ /dev/null @@ -1,100 +0,0 @@ -/* -MIT License - -Copyright (c) 2023 Philipp Schuster - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -//! Abstraction over FFT implementation. This is mainly necessary because we might have -//! `no_std`/`std` implementations as well as real/complex implementations and especially -//! `real` FFT implementations might need a few adjustments. -//! -//! This crate compiles only iff exactly one feature, i.e. one FFT implementation, is activated. -//! -//! ## What FFT implementation to choose? -//! "microfft-real" should be in any way the fastest implementation and fine in any case. -//! I added multiple implementations primarily for educational reasons to myself to learn -//! differences between real and complex FFT. As of release 0.5.0 there is no valid case -//! why you should switch to another FFT implementation. They are primarily useful to me -//! during development to test different FFT implementations and see if my code is correct. -//! -//! ## Tips for development/testing -//! Usually I do all tests against "rustfft" because it is the -//! most actively developed implementation and also the fastest and most accurate, at least -//! in `std`-environments on modern processors. To test other implementations I usually -//! plot the results using the function -//! `crate::tests::test_spectrum_and_visualize_sine_waves_50_1000_3777hz` by invoking it -//! with different features (FFT implementations) enabled. - -#[cfg(feature = "microfft-complex")] -mod microfft_complex; -#[cfg(feature = "microfft-complex")] -pub use microfft_complex::*; - -#[cfg(feature = "microfft-real")] -mod microfft_real; -#[cfg(feature = "microfft-real")] -pub use microfft_real::*; - -#[cfg(feature = "rustfft-complex")] -mod rustfft_complex; -#[cfg(feature = "rustfft-complex")] -pub use rustfft_complex::*; - -use alloc::vec::Vec; - -/// Abstraction over different FFT implementations. This is necessary because this crate -/// uses different compile time features to exchange the FFT implementation, i.e. real or complex. -/// Each of them operates on possibly different "num-complex"-versions for example. -pub(crate) trait Fft { - /// Applies the FFT on the given implementation. If necessary, the data is converted to a - /// complex number first. The resulting vector contains complex numbers without any - /// normalization/scaling. Usually you calc the magnitude of each complex number on the - /// resulting vector to get the amplitudes of the frequencies in the next step. - /// - /// ## Parameters - /// * `samples` samples for FFT. Length MUST be a power of 2 for FFT, e.g. 1024 or 4096! - /// - /// ## Return - /// Vector of FFT results. - fn fft_apply(samples: &[f32]) -> Vec; - - /// Returns the relevant results of the FFT result. For complex FFT this is - /// `N/2 + 1`, i.e. indices `0..=N/2` (inclusive end) are relevant. Real FFT - /// implementations might be different here, because they may only have - /// `N/2` results. - /// - /// The return value of this multiplied with `frequency_resolution` usually - /// refers to the Nyquist frequency. - /// - /// For complex FFT we don't need the second half because it refers to - /// negative frequency values (mirrored to first half with pos frequency values), - /// therefore we skip it; the return value is smaller than `complex_samples.len()`. - /// - /// ## More info - /// * - /// * - /// - /// ## Parameters - /// * `samples_len` Number of samples put into the FFT - /// - /// ## Return value - /// Number of relevant samples. - fn fft_relevant_res_samples_count(samples_len: usize) -> usize; -} diff --git a/src/fft/rustfft_complex.rs b/src/fft/rustfft_complex.rs deleted file mode 100644 index 1f174f5..0000000 --- a/src/fft/rustfft_complex.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* -MIT License - -Copyright (c) 2023 Philipp Schuster - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ -//! Complex FFT using `rustfft`. Results should be equal to the ones from `microfft`. -//! The difference is that this implementation works only in `std`-environments -//! and can handle sample lengths of more than 4096. - -use alloc::vec::Vec; - -use crate::fft::Fft as FftAbstraction; -use rustfft::algorithm::Radix4; -use rustfft::{Fft, FftDirection}; - -/// The result of a FFT is always complex but because different FFT crates might -/// use different versions of "num-complex", each implementation exports -/// it's own version that gets used in lib.rs for binary compatibility. -pub use rustfft::num_complex::Complex32; - -/// Dummy struct with no properties but used as a type -/// to implement a concrete FFT strategy using (`rustfft::algorithm::Radix4`). -pub struct FftImpl; - -impl FftImpl { - /// Converts all samples to a complex number (imaginary part is set to zero) - /// as preparation for the FFT. - /// - /// ## Parameters - /// `samples` Input samples. - /// - /// ## Return value - /// New vector with elements of FFT output/result. - #[inline] - fn samples_to_complex(samples: &[f32]) -> Vec { - samples - .iter() - .map(|x| Complex32::new(*x, 0.0)) - .collect::>() - } -} - -impl FftAbstraction for FftImpl { - #[inline] - fn fft_apply(samples: &[f32]) -> Vec { - let mut samples = Self::samples_to_complex(samples); - let fft = Radix4::new(samples.len(), FftDirection::Forward); - fft.process(&mut samples); - samples - } - - #[inline] - fn fft_relevant_res_samples_count(samples_len: usize) -> usize { - // See https://stackoverflow.com/a/4371627/2891595 for more information as well as - // https://www.gaussianwaves.com/2015/11/interpreting-fft-results-complex-dft-frequency-bins-and-fftshift/ - // - // The indices 0 to N/2 (inclusive) are usually the most relevant. Although, index - // N/2-1 is declared as the last useful one on stackoverflow (because in typical applications - // Nyquist-frequency + above are filtered out), we include everything here. - // with 0..=(samples_len / 2) (inclusive) we get all frequencies from 0 to Nyquist theorem. - // - // Indices (samples_len / 2)..len() are mirrored/negative. You can also see this here: - // https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2015/11/realDFT_complexDFT.png - samples_len / 2 + 1 - } -} diff --git a/src/lib.rs b/src/lib.rs index 07fef41..378c5ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,20 +73,6 @@ SOFTWARE. #![deny(rustdoc::all)] #![no_std] -#[cfg(any( - all(feature = "microfft-real", feature = "microfft-complex"), - all(feature = "microfft-real", feature = "rustfft-complex"), - all(feature = "microfft-complex", feature = "rustfft-complex"), - not(any( - feature = "microfft-real", - feature = "microfft-complex", - feature = "rustfft-complex" - )) -))] -compile_error!( - "You must use exactly one FFT implementation. Check Cargo compile-time features of this crate!" -); - // enable std in tests (println!() for example) #[cfg_attr(test, macro_use)] #[cfg(test)] @@ -100,7 +86,7 @@ extern crate alloc; use alloc::vec::Vec; use crate::error::SpectrumAnalyzerError; -use crate::fft::{Complex32, Fft, FftImpl}; +use crate::fft::{Complex32, FftImpl}; pub use crate::frequency::{Frequency, FrequencyValue}; pub use crate::limit::FrequencyLimit; pub use crate::limit::FrequencyLimitError; @@ -132,7 +118,8 @@ mod tests; /// You should apply an window function (like Hann) on the data first. /// The final frequency resolution is `sample_rate / (N / 2)` /// e.g. `44100/(16384/2) == 5.383Hz`, i.e. more samples => -/// better accuracy/frequency resolution. +/// better accuracy/frequency resolution. The amount of samples must +/// be a power of 2. If you don't have enough data, provide zeroes. /// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]` /// * `frequency_limit` Frequency limit. See [`FrequencyLimit´] /// * `scaling_fn` See [`crate::scaling::SpectrumScalingFunction`] for details. @@ -206,7 +193,7 @@ pub fn samples_fft_to_spectrum( // chosen at compile time (via Cargo feature). // If a complex FFT implementation was chosen, this will internally // transform all data to Complex numbers. - let buffer = FftImpl::fft_apply(samples); + let fft_res = FftImpl::calc(samples); // This function: // 1) calculates the corresponding frequency of each index in the FFT result @@ -216,7 +203,7 @@ pub fn samples_fft_to_spectrum( // 5) collects everything into the struct "FrequencySpectrum" fft_result_to_spectrum( samples.len(), - &buffer, + &fft_res, sampling_rate, frequency_limit, scaling_fn, @@ -230,7 +217,7 @@ pub fn samples_fft_to_spectrum( /// ## Parameters /// * `samples_len` Length of samples. This is a dedicated field because it can't always be /// derived from `fft_result.len()`. There are for example differences for -/// `fft_result.len()` in real and complex FFT. +/// `fft_result.len()` in real and complex FFT algorithms. /// * `fft_result` Result buffer from FFT. Has the same length as the samples array. /// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]` /// * `frequency_limit` Frequency limit. See [`FrequencyLimit´] @@ -264,7 +251,7 @@ fn fft_result_to_spectrum( // // Indices (samples_len / 2)..len() are mirrored/negative. You can also see this here: // https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2015/11/realDFT_complexDFT.png - .take(FftImpl::fft_relevant_res_samples_count(samples_len)) + .take(samples_len / 2 + 1) // to (index, fft-result)-pairs .enumerate() // calc index => corresponding frequency