diff --git a/.editorconfig b/.editorconfig index 75a88ac..4a708d2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,10 @@ insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true +max_line_length = 80 + +[*.nix] +indent_size = 2 [*.yml] indent_size = 2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 070a250..87efede 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,7 +14,7 @@ jobs: rust: - stable - nightly - - 1.56.1 + - 1.61.0 # MSRV steps: - uses: actions/checkout@v2 # Important preparation step: override the latest default Rust version in GitHub CI @@ -39,6 +39,9 @@ jobs: - 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" @@ -55,7 +58,7 @@ jobs: strategy: matrix: rust: - - stable + - 1.61.0 # MSRV steps: - uses: actions/checkout@v2 # Important preparation step: override the latest default Rust version in GitHub CI diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b7d46..f3ed734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 1.3.0 (2022-12-XX) +- dependency updates +- 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 + slightly improved as less heap allocations are required. +- `FrequencySpectrum` is now `Send` and interior mutability is dropped: + You can wrap the struct in a `Mutex` or similar types now! +- `FrequencySpectrum::to_map` doesn't has the `scaling_fn` parameter anymore. + 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) - fixed wrong scaling in `scaling::divide_by_N_sqrt` () diff --git a/Cargo.toml b/Cargo.toml index 05e76f7..736f858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "spectrum-analyzer" description = """ -A simple and fast `no_std` library to get the frequency spectrum of a digital signal (e.g. audio) using FFT. -It follows the KISS principle and consists of simple building blocks/optional features. +An easy to use and fast `no_std` library (with `alloc`) to get the frequency +spectrum of a digital signal (e.g. audio) using FFT. """ -version = "1.2.6" +version = "1.3.0" authors = ["Philipp Schuster "] edition = "2021" keywords = ["fft", "spectrum", "frequencies", "audio", "dsp"] @@ -20,6 +20,10 @@ exclude = [ ".github" ] +[[bench]] +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. @@ -47,12 +51,14 @@ minimp3 = "0.5" # visualize spectrum in tests and examples audio-visualizer = "0.3" # get audio input in examples -cpal = "0.13" +cpal = "0.15.0" # audio data buffering -ringbuffer = "0.8" # REQUIRES Rust Stable 1.55 because it uses "ringbuffer v0.8" +ringbuffer = "0.12.0" rand = "0.8" # for benchmark # exit in examples ctrlc = "3.2" +# for benchmark +criterion = "0.4" # otherwise FFT and other code is too slow diff --git a/LICENSE b/LICENSE index 632258c..4d25037 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/README.md b/README.md index 4531043..6e6d211 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,21 @@ # Rust: library for frequency spectrum analysis using FFT -A simple and fast `no_std` library to get the frequency spectrum of a digital signal (e.g. audio) using FFT. -It follows the KISS principle and consists of simple building blocks/optional features. In short, this is -a convenient wrapper around several FFT implementations which you can choose from during compilation time -via Cargo features. +An easy to use and fast `no_std` library (with `alloc`) to get the frequency +spectrum of a digital signal (e.g. audio) using FFT. -**I'm not an expert on digital signal processing. Code contributions are highly welcome! 🙂** - -The **MSRV** (minimum supported Rust version) is 1.56.1 stable, because this crate uses -Rust edition 2021. +The **MSRV** (minimum supported Rust version) is `1.61.0`. ## I want to understand how FFT can be used to get a spectrum Please see file [/EDUCATIONAL.md](/EDUCATIONAL.md). ## How to use (including `no_std`-environments) -Most tips and comments are located inside the code, so please check out the repository on -Github! Anyway, the most basic usage looks like this: +Most tips and comments are located inside the code, so please check out the +repository on GitHub! Anyway, the most basic usage looks like this: ### FFT implementation as compile time configuration via Cargo features -By default this crate uses the `real`-module from the great `microfft`-crate. It's the fastest implementation -and as of version `v0.5.0` there should be no valid reason why you should ever change this. The multiple features -are there mainly for educational reasons and to support me while programming/testing. +By default, this crate uses the `real`-module from the great `microfft`-crate. +It's the fastest implementation and as of version `v0.5.0` there should be no +valid reason why you should ever change this. The multiple features are there +mainly for educational reasons and to support me during programming/testing. ### Cargo.toml ```toml @@ -68,7 +64,6 @@ fn main() { ## Performance *Measurements taken on i7-1165G7 @ 2.80GHz (Single-threaded) with optimized build* - | Operation | Time | | ------------------------------------------------------ | ------:| | Hann Window with 4096 samples | ≈68µs | @@ -78,8 +73,8 @@ fn main() { | 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 -a layered signal of sine waves of `50`, `1000`, and `3777Hz` @ `44100Hz` sampling rate. The peaks for the +In the following examples you can see a basic visualization of the spectrum from `0 to 4000Hz` for +a layered signal of sine waves of `50`, `1000`, and `3777Hz` @ `44100Hz` sampling rate. The peaks for the given frequencies are clearly visible. Each calculation was done with `2048` samples, i.e. ≈46ms of audio signal. **The noise (wrong peaks) also comes from clipping of the added sine waves!** @@ -89,34 +84,38 @@ Peaks (50, 1000, 3777 Hz) are clearly visible but also some noise. ![Visualization of spectrum 0-4000Hz of layered sine signal (50, 1000, 3777 Hz)) with no window function.](res/spectrum_sine_waves_50_1000_3777hz--no-window.png "Peaks (50, 1000, 3777 Hz) are clearly visible but also some noise.") ### Spectrum with *Hann window function* on samples before FFT -Peaks (50, 1000, 3777 Hz) are clearly visible and Hann window reduces noise a little bit. Because this example has few noise, you don't see much difference. +Peaks (50, 1000, 3777 Hz) are clearly visible and Hann window reduces noise a +little. Because this example has few noise, you don't see much difference. ![Visualization of spectrum 0-4000Hz of layered sine signal (50, 1000, 3777 Hz)) with Hann window function.](res/spectrum_sine_waves_50_1000_3777hz--hann-window.png "Peaks (50, 1000, 3777 Hz) are clearly visible and Hann window reduces noise a little bit. Because this example has few noise, you don't see much difference.") ### Spectrum with *Hamming window function* on samples before FFT -Peaks (50, 1000, 3777 Hz) are clearly visible and Hamming window reduces noise a little bit. Because this example has few noise, you don't see much difference. +Peaks (50, 1000, 3777 Hz) are clearly visible and Hamming window reduces noise a +little. Because this example has few noise, you don't see much difference. ![Visualization of spectrum 0-4000Hz of layered sine signal (50, 1000, 3777 Hz)) with Hamming window function.](res/spectrum_sine_waves_50_1000_3777hz--hamming-window.png "Peaks (50, 1000, 3777 Hz) are clearly visible and Hamming window reduces noise a little bit. Because this example has few noise, you don't see much difference.") ## Live Audio + Spectrum Visualization -Execute example `$ cargo run --release --example live-visualization`. It will show you -how you can visualize audio data in realtime + the current spectrum. +Execute example `$ cargo run --release --example live-visualization`. It will +show you how you can visualize audio data in realtime + the current spectrum. ![Example visualization of real-time audio + spectrum analysis](res/live_demo_spectrum_green_day_holiday.gif "Example visualization of real-time audio + spectrum analysis") ## Building and Executing Tests -To execute tests you need the package `libfreetype6-dev` (on Ubuntu/Debian). This is required because -not all tests are "automatic unit tests" but also tests that you need to check visually, by looking at the -generated diagram of the spectrum. +To execute tests you need the package `libfreetype6-dev` (on Ubuntu/Debian). +This is required because not all tests are "automatic unit tests" but also tests +that you need to check visually, by looking at the generated diagram of the +spectrum. ## Trivia / FAQ ### Why f64 and no f32? -I tested f64 but the additional accuracy doesn't pay out the ~40% calculation overhead (on x86_64). +I tested f64 but the additional accuracy doesn't pay out the ~40% calculation +overhead (on x86_64). ### What can I do against the noise? -Apply a window function, like Hann window or Hamming window. But I'm not an expert on this. +Apply a window function, like Hann window or Hamming window. ## Good resources with more information -- Interpreting FFT Results: https://www.gaussianwaves.com/2015/11/interpreting-fft-results-complex-dft-frequency-bins-and-fftshift/ -- FFT basic concepts: https://www.youtube.com/watch?v=z7X6jgFnB6Y -- „The Fundamentals of FFT-Based Signal Analysis and Measurement“ https://www.sjsu.edu/people/burford.furman/docs/me120/FFT_tutorial_NI.pdf -- Fast Fourier Transforms (FFTs) and Windowing: https://www.youtube.com/watch?v=dCeHOf4cJE0 +- Interpreting FFT Results: +- FFT basic concepts: +- „The Fundamentals of FFT-Based Signal Analysis and Measurement“ +- Fast Fourier Transforms (FFTs) and Windowing: -Also check out my blog post! https://phip1611.de/2021/03/programmierung-und-skripte/frequency-spectrum-analysis-with-fft-in-rust/ +Also check out my [blog post](https://phip1611.de/2021/03/programmierung-und-skripte/frequency-spectrum-analysis-with-fft-in-rust/). diff --git a/benches/fft_spectrum_bench.rs b/benches/fft_spectrum_bench.rs new file mode 100644 index 0000000..e045d9f --- /dev/null +++ b/benches/fft_spectrum_bench.rs @@ -0,0 +1,57 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use spectrum_analyzer::{ + samples_fft_to_spectrum, scaling, windows, FrequencyLimit, FrequencySpectrum, +}; + +fn spectrum_without_scaling(samples: &[f32]) -> FrequencySpectrum { + samples_fft_to_spectrum(samples, 44100, FrequencyLimit::All, None).unwrap() +} + +fn spectrum_with_scaling(samples: &[f32]) -> FrequencySpectrum { + samples_fft_to_spectrum( + samples, + 44100, + FrequencyLimit::All, + Some(&scaling::divide_by_N_sqrt), + ) + .unwrap() +} + +fn spectrum_with_multiple_scaling(samples: &[f32]) -> FrequencySpectrum { + let mut spectrum = spectrum_with_scaling(samples); + + let mut working_buffer = vec![(0.0.into(), 0.0.into()); spectrum.data().len()]; + + spectrum + .apply_scaling_fn(&scaling::divide_by_N_sqrt, &mut working_buffer) + .unwrap(); + spectrum + .apply_scaling_fn(&scaling::divide_by_N_sqrt, &mut working_buffer) + .unwrap(); + spectrum + .apply_scaling_fn(&scaling::divide_by_N_sqrt, &mut working_buffer) + .unwrap(); + spectrum +} + +fn criterion_benchmark(c: &mut Criterion) { + // create 2048 random samples + let samples = (0..2048) + .map(|_| rand::random::()) + .map(|x| x as f32) + .collect::>(); + let hann_window = windows::hann_window(&samples); + + c.bench_function("spectrum without scaling", |b| { + b.iter(|| spectrum_without_scaling(black_box(&hann_window))) + }); + c.bench_function("spectrum with scaling", |b| { + b.iter(|| spectrum_without_scaling(black_box(&hann_window))) + }); + c.bench_function("spectrum with multiple scaling steps", |b| { + b.iter(|| spectrum_with_multiple_scaling(black_box(&hann_window))) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/check-build.sh b/check-build.sh index 5cb28df..b16eb2d 100755 --- a/check-build.sh +++ b/check-build.sh @@ -12,6 +12,8 @@ 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 diff --git a/examples/bench.rs b/examples/bench.rs index 207a70b..635875a 100644 --- a/examples/bench.rs +++ b/examples/bench.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/examples/live-visualization.rs b/examples/live-visualization.rs index 4cf5bcd..4aff1e8 100644 --- a/examples/live-visualization.rs +++ b/examples/live-visualization.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/examples/minimal.rs b/examples/minimal.rs index d8e7616..dfb19b4 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/examples/mp3-samples.rs b/examples/mp3-samples.rs index 84b35a5..2f976e7 100644 --- a/examples/mp3-samples.rs +++ b/examples/mp3-samples.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -212,31 +212,31 @@ fn to_spectrum_and_plot( }*/ spectrum_static_plotters_png_visualize( - &spectrum_no_window.to_map(None), + &spectrum_no_window.to_map(), TEST_OUT_DIR, &format!("{}--no-window.png", filename), ); spectrum_static_plotters_png_visualize( - &spectrum_hamming_window.to_map(None), + &spectrum_hamming_window.to_map(), TEST_OUT_DIR, &format!("{}--hamming-window.png", filename), ); spectrum_static_plotters_png_visualize( - &spectrum_hann_window.to_map(None), + &spectrum_hann_window.to_map(), TEST_OUT_DIR, &format!("{}--hann-window.png", filename), ); spectrum_static_plotters_png_visualize( - &spectrum_blackman_harris_4term_window.to_map(None), + &spectrum_blackman_harris_4term_window.to_map(), TEST_OUT_DIR, &format!("{}--blackman-harris-4-term-window.png", filename), ); spectrum_static_plotters_png_visualize( - &spectrum_blackman_harris_7term_window.to_map(None), + &spectrum_blackman_harris_7term_window.to_map(), TEST_OUT_DIR, &format!("{}--blackman-harris-7-term-window.png", filename), ); diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index e3718b4..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,9 +0,0 @@ -# doesn't used in CI - -[toolchain] -# https://rust-lang.github.io/rustup/concepts/profiles.html -# makes sure that "clippy" and "rustfmt" get downloaded and installed -# when "cargo fmt" or "cargo clippy" gets invoked. -profile = "default" -channel = "1.56.1" # msvr also in README - diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..6878552 --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: + pkgs.mkShell rec { + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + + buildInputs = with pkgs; [ + alsa-lib + fontconfig + libxkbcommon + xorg.libXcursor + xorg.libX11 + ]; + + LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}"; +} diff --git a/src/error.rs b/src/error.rs index c99e683..b2c1711 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/src/fft/microfft_complex.rs b/src/fft/microfft_complex.rs index 2a8d91c..0f69de1 100644 --- a/src/fft/microfft_complex.rs +++ b/src/fft/microfft_complex.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -49,7 +49,7 @@ impl FftImpl { /// /// ## Return value /// New vector with elements of FFT output/result. - #[inline(always)] + #[inline] fn samples_to_complex(samples: &[f32]) -> Vec { samples .iter() @@ -59,7 +59,7 @@ impl FftImpl { } impl Fft for FftImpl { - #[inline(always)] + #[inline] fn fft_apply(samples: &[f32]) -> Vec { let buffer = Self::samples_to_complex(samples); @@ -104,7 +104,7 @@ impl Fft for FftImpl { } } - #[inline(always)] + #[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/ diff --git a/src/fft/microfft_real.rs b/src/fft/microfft_real.rs index efaa8b6..b933d5d 100644 --- a/src/fft/microfft_real.rs +++ b/src/fft/microfft_real.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -41,7 +41,7 @@ pub use microfft::Complex32; pub struct FftImpl; impl Fft for FftImpl { - #[inline(always)] + #[inline] fn fft_apply(samples: &[f32]) -> Vec { let buffer = samples; let mut res = { @@ -103,7 +103,7 @@ impl Fft for FftImpl { res } - #[inline(always)] + #[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 diff --git a/src/fft/mod.rs b/src/fft/mod.rs index 7710ca0..06c0849 100644 --- a/src/fft/mod.rs +++ b/src/fft/mod.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -88,8 +88,8 @@ pub(crate) trait Fft { /// therefore we skip it; the return value is smaller than `complex_samples.len()`. /// /// ## More info - /// * https://www.researchgate.net/post/How-can-I-define-the-frequency-resolution-in-FFT-And-what-is-the-difference-on-interpreting-the-results-between-high-and-low-frequency-resolution - /// * https://stackoverflow.com/questions/4364823/ + /// * + /// * /// /// ## Parameters /// * `samples_len` Number of samples put into the FFT diff --git a/src/fft/rustfft_complex.rs b/src/fft/rustfft_complex.rs index 2c5af21..1f174f5 100644 --- a/src/fft/rustfft_complex.rs +++ b/src/fft/rustfft_complex.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -49,7 +49,7 @@ impl FftImpl { /// /// ## Return value /// New vector with elements of FFT output/result. - #[inline(always)] + #[inline] fn samples_to_complex(samples: &[f32]) -> Vec { samples .iter() @@ -59,7 +59,7 @@ impl FftImpl { } impl FftAbstraction for FftImpl { - #[inline(always)] + #[inline] fn fft_apply(samples: &[f32]) -> Vec { let mut samples = Self::samples_to_complex(samples); let fft = Radix4::new(samples.len(), FftDirection::Forward); @@ -67,7 +67,7 @@ impl FftAbstraction for FftImpl { samples } - #[inline(always)] + #[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/ diff --git a/src/frequency.rs b/src/frequency.rs index 64aa099..b1ff06b 100644 --- a/src/frequency.rs +++ b/src/frequency.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -28,28 +28,26 @@ use core::cmp::Ordering; use core::fmt::{Display, Formatter, Result}; use core::ops::{Add, Div, Mul, Sub}; -/// A frequency. A convenient wrapper type around `f32`. +/// A frequency in Hertz. A convenient wrapper type around `f32`. pub type Frequency = OrderableF32; -/// The value of a frequency in a frequency spectrum. Convenient wrapper around `f32`. -/// Not necessarily the magnitude of the complex numbers because scaling/normalization -/// functions could have been applied. +/// The value of a [`Frequency`] in a frequency spectrum. Also called the +/// magnitude. pub type FrequencyValue = OrderableF32; -/// Small convenient wrapper around `f32`. -/// Mainly required to make `f32` operable in a sorted tree map. -/// You should only use the type aliases `Frequency` and `FrequencyValue`. +/// Wrapper around [`f32`] that guarantees a valid number, hence, the number is +/// neither `NaN` or `infinite`. This makes the number orderable and sortable. #[derive(Debug, Copy, Clone, Default)] pub struct OrderableF32(f32); impl OrderableF32 { - #[inline(always)] + #[inline] pub const fn val(&self) -> f32 { self.0 } } impl From for OrderableF32 { - #[inline(always)] + #[inline] fn from(val: f32) -> Self { debug_assert!(!val.is_nan(), "NaN-values are not supported!"); debug_assert!(!val.is_infinite(), "Infinite-values are not supported!"); @@ -64,7 +62,7 @@ impl Display for OrderableF32 { } impl Ord for OrderableF32 { - #[inline(always)] + #[inline] fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap() } @@ -73,7 +71,7 @@ impl Ord for OrderableF32 { impl Eq for OrderableF32 {} impl PartialEq for OrderableF32 { - #[inline(always)] + #[inline] fn eq(&self, other: &Self) -> bool { matches!(self.cmp(other), Ordering::Equal) } @@ -81,7 +79,7 @@ impl PartialEq for OrderableF32 { impl PartialOrd for OrderableF32 { #[allow(clippy::float_cmp)] - #[inline(always)] + #[inline] fn partial_cmp(&self, other: &Self) -> Option { // self.cmp(other).is_eq() Some(if self.val() < other.val() { @@ -97,7 +95,7 @@ impl PartialOrd for OrderableF32 { impl Add for OrderableF32 { type Output = Self; - #[inline(always)] + #[inline] fn add(self, other: Self) -> Self::Output { (self.val() + other.val()).into() } @@ -106,7 +104,7 @@ impl Add for OrderableF32 { impl Sub for OrderableF32 { type Output = Self; - #[inline(always)] + #[inline] fn sub(self, other: Self) -> Self::Output { (self.val() - other.val()).into() } @@ -115,7 +113,7 @@ impl Sub for OrderableF32 { impl Mul for OrderableF32 { type Output = Self; - #[inline(always)] + #[inline] fn mul(self, other: Self) -> Self::Output { (self.val() * other.val()).into() } @@ -124,7 +122,7 @@ impl Mul for OrderableF32 { impl Div for OrderableF32 { type Output = Self; - #[inline(always)] + #[inline] fn div(self, other: Self) -> Self::Output { let quotient = self.val() / other.val(); debug_assert!(!quotient.is_nan(), "NaN is not allowed"); diff --git a/src/lib.rs b/src/lib.rs index 7375ce9..07fef41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -21,13 +21,8 @@ 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. */ -//! A simple and fast `no_std` library to get the frequency spectrum of a digital signal -//! (e.g. audio) using FFT. It follows the KISS principle and consists of simple building -//! blocks/optional features. -//! -//! In short, this is a convenient wrapper around a FFT implementation. You choose the -//! implementation at compile time via Cargo features. This crate uses -//! "microfft" by default for FFT. See README for more advise. +//! An easy to use and fast `no_std` library (with `alloc`) to get the frequency +//! spectrum of a digital signal (e.g. audio) using FFT. //! //! ## Examples //! ### Scaling via dynamic closure @@ -45,14 +40,15 @@ SOFTWARE. //! ### Scaling via static function //! ```rust //! use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit}; -//! use spectrum_analyzer::scaling::scale_to_zero_to_one; +//! use spectrum_analyzer::scaling::divide_by_N_sqrt; //! // get data from audio source //! let samples = vec![0.0, 1.1, 5.5, -5.5]; //! let res = samples_fft_to_spectrum( //! &samples, //! 44100, //! FrequencyLimit::All, -//! Some(&scale_to_zero_to_one), +//! // Recommended scaling/normalization by `rustfft`. +//! Some(÷_by_N_sqrt), //! ); //! ``` @@ -60,6 +56,7 @@ SOFTWARE. clippy::all, clippy::cargo, clippy::nursery, + clippy::must_use_candidate // clippy::restriction, // clippy::pedantic )] @@ -97,7 +94,7 @@ extern crate std; // We use alloc crate, because this is no_std // The macros are only needed when we test -#[cfg_attr(test, macro_use)] +#[macro_use] extern crate alloc; use alloc::vec::Vec; @@ -241,7 +238,7 @@ pub fn samples_fft_to_spectrum( /// /// ## Return value /// New object of type [`FrequencySpectrum`]. -#[inline(always)] +#[inline] fn fft_result_to_spectrum( samples_len: usize, fft_result: &[Complex32], @@ -330,12 +327,19 @@ fn fft_result_to_spectrum( // collect all into an sorted vector (from lowest frequency to highest) .collect::>(); + let mut working_buffer = vec![(0.0.into(), 0.0.into()); frequency_vec.len()]; + // create spectrum object - let spectrum = FrequencySpectrum::new(frequency_vec, frequency_resolution, samples_len as u32); + let mut spectrum = FrequencySpectrum::new( + frequency_vec, + frequency_resolution, + samples_len as u32, + &mut working_buffer, + ); // optionally scale if let Some(scaling_fn) = scaling_fn { - spectrum.apply_scaling_fn(scaling_fn)? + spectrum.apply_scaling_fn(scaling_fn, &mut working_buffer)? } Ok(spectrum) @@ -355,9 +359,9 @@ fn fft_result_to_spectrum( /// Frequency resolution in Hertz. /// /// ## More info -/// * https://www.researchgate.net/post/How-can-I-define-the-frequency-resolution-in-FFT-And-what-is-the-difference-on-interpreting-the-results-between-high-and-low-frequency-resolution -/// * https://stackoverflow.com/questions/4364823/ -#[inline(always)] +/// * +/// * +#[inline] fn fft_calc_frequency_resolution(sampling_rate: u32, samples_len: u32) -> f32 { sampling_rate as f32 / samples_len as f32 } diff --git a/src/limit.rs b/src/limit.rs index 94e884c..3aafbed 100644 --- a/src/limit.rs +++ b/src/limit.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -49,7 +49,8 @@ pub enum FrequencyLimit { impl FrequencyLimit { /// Returns the minimum value, if any. - #[inline(always)] + #[inline] + #[must_use] pub const fn maybe_min(&self) -> Option { match self { Self::Min(min) => Some(*min), @@ -59,7 +60,8 @@ impl FrequencyLimit { } /// Returns the maximum value, if any. - #[inline(always)] + #[inline] + #[must_use] pub const fn maybe_max(&self) -> Option { match self { Self::Max(max) => Some(*max), @@ -70,14 +72,16 @@ impl FrequencyLimit { /// Returns the minimum value, panics if it's none. /// Unwrapped version of [`Self::maybe_min`]. - #[inline(always)] + #[inline] + #[must_use] pub fn min(&self) -> f32 { self.maybe_min().expect("Must contain a value!") } /// Returns the minimum value, panics if it's none. /// Unwrapped version of [`Self::maybe_max`]. - #[inline(always)] + #[inline] + #[must_use] pub fn max(&self) -> f32 { self.maybe_max().expect("Must contain a value!") } @@ -163,10 +167,10 @@ mod tests { #[test] fn test_ok() { - let _ = FrequencyLimit::Min(50.0).verify(100.0).unwrap(); - let _ = FrequencyLimit::Max(50.0).verify(100.0).unwrap(); + FrequencyLimit::Min(50.0).verify(100.0).unwrap(); + FrequencyLimit::Max(50.0).verify(100.0).unwrap(); // useless, but not an hard error - let _ = FrequencyLimit::Range(50.0, 50.0).verify(100.0).unwrap(); - let _ = FrequencyLimit::Range(50.0, 70.0).verify(100.0).unwrap(); + FrequencyLimit::Range(50.0, 50.0).verify(100.0).unwrap(); + FrequencyLimit::Range(50.0, 70.0).verify(100.0).unwrap(); } } diff --git a/src/scaling.rs b/src/scaling.rs index 0e12db8..0267d5c 100644 --- a/src/scaling.rs +++ b/src/scaling.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -28,10 +28,10 @@ SOFTWARE. use alloc::boxed::Box; -/// Helper struct for [`SpectrumScalingFunction`], that gets passed into the -/// function together with the actual value. This structure can be used to scale -/// each value. All properties reference the current data of a -/// [`crate::spectrum::FrequencySpectrum`]. +/// Helper struct for [`SpectrumScalingFunction`] that is passed into the +/// scaling function together with the current frequency value. This structure +/// can be used to scale each value. All properties reference the current data +/// of a [`crate::spectrum::FrequencySpectrum`]. /// /// This uses `f32` in favor of [`crate::FrequencyValue`] because the latter led to /// some implementation problems. @@ -45,8 +45,8 @@ pub struct SpectrumDataStats { pub average: f32, /// Median frequency value in spectrum. pub median: f32, - /// Number of samples (`samples.len()`). Already - /// casted to f32, to avoid repeatedly casting in a loop for each value. + /// Number of samples (`samples.len()`). Already casted to f32, to avoid + /// repeatedly casting in a loop for each value. pub n: f32, } @@ -83,28 +83,28 @@ pub type SpectrumScalingFunction = dyn Fn(f32, &SpectrumDataStats) -> f32; /// ); /// ``` /// Function is of type [`SpectrumScalingFunction`]. -pub fn scale_20_times_log10(frequency_magnitude: f32, _stats: &SpectrumDataStats) -> f32 { - debug_assert!(!frequency_magnitude.is_infinite()); - debug_assert!(!frequency_magnitude.is_nan()); - debug_assert!(frequency_magnitude >= 0.0); - if frequency_magnitude == 0.0 { +#[must_use] +pub fn scale_20_times_log10(fr_val: f32, _stats: &SpectrumDataStats) -> f32 { + debug_assert!(!fr_val.is_infinite()); + debug_assert!(!fr_val.is_nan()); + debug_assert!(fr_val >= 0.0); + if fr_val == 0.0 { 0.0 } else { - 20.0 * libm::log10f(frequency_magnitude) + 20.0 * libm::log10f(fr_val) } } /// Scales each frequency value/amplitude in the spectrum to interval `[0.0; 1.0]`. /// Function is of type [`SpectrumScalingFunction`]. Expects that [`SpectrumDataStats::min`] is /// not negative. -pub fn scale_to_zero_to_one(val: f32, stats: &SpectrumDataStats) -> f32 { - // usually not the case, except you use other scaling functions first, - // that transforms the value to a negative one - /*if stats.min < 0.0 { - val = val + stats.min; - }*/ +#[must_use] +pub fn scale_to_zero_to_one(fr_val: f32, stats: &SpectrumDataStats) -> f32 { + debug_assert!(!fr_val.is_infinite()); + debug_assert!(!fr_val.is_nan()); + debug_assert!(fr_val >= 0.0); if stats.max != 0.0 { - val / stats.max + fr_val / stats.max } else { 0.0 } @@ -113,11 +113,15 @@ pub fn scale_to_zero_to_one(val: f32, stats: &SpectrumDataStats) -> f32 { /// Divides each value by N. Several resources recommend that the FFT result should be divided /// by the length of samples, so that values of different samples lengths are comparable. #[allow(non_snake_case)] -pub fn divide_by_N(val: f32, stats: &SpectrumDataStats) -> f32 { +#[must_use] +pub fn divide_by_N(fr_val: f32, stats: &SpectrumDataStats) -> f32 { + debug_assert!(!fr_val.is_infinite()); + debug_assert!(!fr_val.is_nan()); + debug_assert!(fr_val >= 0.0); if stats.n == 0.0 { - val + fr_val } else { - val / stats.n + fr_val / stats.n } } @@ -125,12 +129,16 @@ pub fn divide_by_N(val: f32, stats: &SpectrumDataStats) -> f32 { /// in the `rustfft` documentation (but is generally applicable). /// See #[allow(non_snake_case)] -pub fn divide_by_N_sqrt(val: f32, stats: &SpectrumDataStats) -> f32 { +#[must_use] +pub fn divide_by_N_sqrt(fr_val: f32, stats: &SpectrumDataStats) -> f32 { + debug_assert!(!fr_val.is_infinite()); + debug_assert!(!fr_val.is_nan()); + debug_assert!(fr_val >= 0.0); if stats.n == 0.0 { - val + fr_val } else { // https://docs.rs/rustfft/latest/rustfft/#normalization - val / libm::sqrtf(stats.n) + fr_val / libm::sqrtf(stats.n) } } @@ -140,7 +148,8 @@ pub fn divide_by_N_sqrt(val: f32, stats: &SpectrumDataStats) -> f32 { /// a `'static` lifetime. This will be fixed if someone needs this. /// /// # Example -/// ```ignored +/// ``` +/// use spectrum_analyzer::scaling::{combined, divide_by_N, scale_20_times_log10}; /// let fncs = combined(&[÷_by_N, &scale_20_times_log10]); /// ``` pub fn combined(fncs: &'static [&SpectrumScalingFunction]) -> Box { @@ -186,7 +195,7 @@ mod tests { let _combined_static = combined(&[&scale_20_times_log10, ÷_by_N, ÷_by_N_sqrt]); // doesn't compile yet.. fix this once someone requests it - /*let closure_scaling_fnc = |frequency_magnitude: f32, _stats: &SpectrumDataStats| { + /*let closure_scaling_fnc = |fr_val: f32, _stats: &SpectrumDataStats| { 0.0 }; diff --git a/src/spectrum.rs b/src/spectrum.rs index 649d729..7b1a76f 100644 --- a/src/spectrum.rs +++ b/src/spectrum.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -23,51 +23,54 @@ SOFTWARE. */ //! Module for the struct [`FrequencySpectrum`]. +use self::math::*; use crate::error::SpectrumAnalyzerError; use crate::frequency::{Frequency, FrequencyValue}; use crate::scaling::{SpectrumDataStats, SpectrumScalingFunction}; use alloc::collections::BTreeMap; use alloc::vec::Vec; -use core::cell::{Cell, Ref, RefCell}; -/// Convenient wrapper around the processed FFT result which describes each frequency and -/// its value/amplitude in the analyzed slice of samples. It only consists of the frequencies -/// which were desired, e.g. specified via -/// [`crate::limit::FrequencyLimit`] when [`crate::samples_fft_to_spectrum`] was called. +/// Convenient wrapper around the processed FFT result which describes each +/// frequency and its value/amplitude from the analyzed samples. It only +/// contains the frequencies that were desired, e.g., specified via +/// [`crate::limit::FrequencyLimit`] when [`crate::samples_fft_to_spectrum`] +/// was called. /// -/// This means, the spectrum can cover all data from the DC component (0Hz) to the -/// Nyquist frequency. +/// This means, the spectrum can cover all data from the DC component (0Hz) to +/// the Nyquist frequency. /// -/// All results are related to the sampling rate provided to the library function which -/// creates objects of this struct! +/// All results are related to the sampling rate provided to the library +/// function which creates objects of this struct! +/// +/// This struct can be shared across thread boundaries. #[derive(Debug, Default)] pub struct FrequencySpectrum { - /// Raw data. Vector is sorted from lowest + /// All (Frequency, FrequencyValue) data pairs sorted by lowest frequency + /// to the highest frequency.Vector is sorted from lowest /// frequency to highest and data is normalized/scaled /// according to all applied scaling functions. - data: RefCell>, + data: Vec<(Frequency, FrequencyValue)>, /// Frequency resolution of the examined samples in Hertz, /// i.e the frequency steps between elements in the vector /// inside field [`Self::data`]. frequency_resolution: f32, - /// Number of samples. This property must be kept separately, because - /// `data.borrow().len()` might contain less than N elements, if the - /// spectrum was created with a [`crate::limit::FrequencyLimit`] . + /// Number of samples that were analyzed. Might be bigger than the length + /// of `data`, if the spectrum was created with a [`crate::limit::FrequencyLimit`] . samples_len: u32, /// Average value of frequency value/magnitude/amplitude /// corresponding to data in [`FrequencySpectrum::data`]. - average: Cell, + average: FrequencyValue, /// Median value of frequency value/magnitude/amplitude /// corresponding to data in [`FrequencySpectrum::data`]. - median: Cell, + median: FrequencyValue, /// Pair of (frequency, frequency value/magnitude/amplitude) where /// frequency value is **minimal** inside the spectrum. /// Corresponding to data in [`FrequencySpectrum::data`]. - min: Cell<(Frequency, FrequencyValue)>, + min: (Frequency, FrequencyValue), /// Pair of (frequency, frequency value/magnitude/amplitude) where /// frequency value is **maximum** inside the spectrum. /// Corresponding to data in [`FrequencySpectrum::data`]. - max: Cell<(Frequency, FrequencyValue)>, + max: (Frequency, FrequencyValue), } impl FrequencySpectrum { @@ -80,11 +83,15 @@ impl FrequencySpectrum { /// `data[1].0 - data[0].0`. /// * `samples_len` Number of samples. Might be bigger than `data.len()` /// if the spectrum is obtained with a frequency limit. - #[inline(always)] + /// * `working_buffer` Mutable buffer with the same length as `data` + /// required to calculate certain metrics. + #[inline] + #[must_use] pub fn new( data: Vec<(Frequency, FrequencyValue)>, frequency_resolution: f32, samples_len: u32, + working_buffer: &mut [(Frequency, FrequencyValue)], ) -> Self { debug_assert!( data.len() >= 2, @@ -92,111 +99,129 @@ impl FrequencySpectrum { data.len() ); - let obj = Self { - data: RefCell::new(data), + let mut obj = Self { + data, frequency_resolution, samples_len, // default/placeholder values - average: Cell::new(FrequencyValue::from(-1.0)), - median: Cell::new(FrequencyValue::from(-1.0)), - min: Cell::new((Frequency::from(-1.0), FrequencyValue::from(-1.0))), - max: Cell::new((Frequency::from(-1.0), FrequencyValue::from(-1.0))), + average: FrequencyValue::from(-1.0), + median: FrequencyValue::from(-1.0), + min: (Frequency::from(-1.0), FrequencyValue::from(-1.0)), + max: (Frequency::from(-1.0), FrequencyValue::from(-1.0)), }; - // IMPORTANT!! - obj.calc_statistics(); + + // Important to call this once initially. + obj.calc_statistics(working_buffer); obj } - /// Applies the function `scaling_fn` to each element and updates - /// `min`, `max`, etc. afterwards accordingly. It ensures that no value - /// is `NaN` or `Infinity` afterwards (regarding IEEE-754). + /// Applies the function `scaling_fn` to each element and updates several + /// metrics about the spectrum, such as `min` and `max`, afterwards + /// accordingly. It ensures that no value is `NaN` or `Infinity` + /// (regarding IEEE-754) after `scaling_fn` was applied. Otherwise, + /// `SpectrumAnalyzerError::ScalingError` is returned. /// /// ## Parameters /// * `scaling_fn` See [`crate::scaling::SpectrumScalingFunction`]. - #[inline(always)] + #[inline] pub fn apply_scaling_fn( - &self, + &mut self, scaling_fn: &SpectrumScalingFunction, + working_buffer: &mut [(Frequency, FrequencyValue)], ) -> Result<(), SpectrumAnalyzerError> { - { - // drop RefMut<> from borrow_mut() before calc_statistics - let mut data = self.data.borrow_mut(); - - let stats = SpectrumDataStats { - min: self.min.get().1.val(), - max: self.max.get().1.val(), - average: self.average.get().val(), - median: self.median.get().val(), - // attention! not necessarily `data.len()`! - n: self.samples_len as f32, - }; + // This represents statistics about the spectrum in its current state + // which a scaling function may use to scale values. + // + // On the first invocation of this function, these values represent the + // statistics for the unscaled, hence initial, spectrum. + let stats = SpectrumDataStats { + min: self.min.1.val(), + max: self.max.1.val(), + average: self.average.val(), + median: self.median.val(), + // attention! not necessarily `data.len()`! + n: self.samples_len as f32, + }; - for (_fr, fr_val) in &mut *data { - // regular for instead of for_each(), so that I can early return a result here - let scaled_val: f32 = scaling_fn(fr_val.val(), &stats); - if scaled_val.is_nan() || scaled_val.is_infinite() { - return Err(SpectrumAnalyzerError::ScalingError( - fr_val.val(), - scaled_val, - )); - } - *fr_val = scaled_val.into() + // Iterate over the whole spectrum and scale each frequency value. + // I use a regular for loop instead of for_each(), so that I can + // early return a result here + for (_fr, fr_val) in &mut self.data { + // scale value + let scaled_val: f32 = scaling_fn(fr_val.val(), &stats); + + // sanity check + if scaled_val.is_nan() || scaled_val.is_infinite() { + return Err(SpectrumAnalyzerError::ScalingError( + fr_val.val(), + scaled_val, + )); } - // drop RefMut<> from borrow_mut() before calc_statistics + + // Update value in spectrum + *fr_val = scaled_val.into() } - self.calc_statistics(); + self.calc_statistics(working_buffer); Ok(()) } /// Returns the average frequency value of the spectrum. - #[inline(always)] - pub fn average(&self) -> FrequencyValue { - self.average.get() + #[inline] + #[must_use] + pub const fn average(&self) -> FrequencyValue { + self.average } /// Returns the median frequency value of the spectrum. - #[inline(always)] - pub fn median(&self) -> FrequencyValue { - self.median.get() + #[inline] + #[must_use] + pub const fn median(&self) -> FrequencyValue { + self.median } /// Returns the maximum (frequency, frequency value)-pair of the spectrum - /// (regarding the frequency value). - #[inline(always)] - pub fn max(&self) -> (Frequency, FrequencyValue) { - self.max.get() + /// **regarding the frequency value**. + #[inline] + #[must_use] + pub const fn max(&self) -> (Frequency, FrequencyValue) { + self.max } /// Returns the minimum (frequency, frequency value)-pair of the spectrum - /// (regarding the frequency value). - #[inline(always)] - pub fn min(&self) -> (Frequency, FrequencyValue) { - self.min.get() + /// **regarding the frequency value**. + #[inline] + #[must_use] + pub const fn min(&self) -> (Frequency, FrequencyValue) { + self.min } /// Returns [`FrequencySpectrum::max().1`] - [`FrequencySpectrum::min().1`], /// i.e. the range of the frequency values (not the frequencies itself, /// but their amplitudes/values). - #[inline(always)] + #[inline] + #[must_use] pub fn range(&self) -> FrequencyValue { self.max().1 - self.min().1 } /// Returns the underlying data. - #[inline(always)] - pub fn data(&self) -> Ref> { - self.data.borrow() + #[inline] + #[must_use] + pub fn data(&self) -> &[(Frequency, FrequencyValue)] { + &self.data } /// Returns the frequency resolution of this spectrum. - #[inline(always)] + #[inline] + #[must_use] pub const fn frequency_resolution(&self) -> f32 { self.frequency_resolution } /// Returns the number of samples used to obtain this spectrum. - #[inline(always)] + #[inline] + #[must_use] pub const fn samples_len(&self) -> u32 { self.samples_len } @@ -207,10 +232,10 @@ impl FrequencySpectrum { /// /// This method could return the Nyquist frequency, if there was no Frequency /// limit while obtaining the spectrum. - #[inline(always)] + #[inline] + #[must_use] pub fn max_fr(&self) -> Frequency { - let data = self.data.borrow(); - data[data.len() - 1].0 + self.data[self.data.len() - 1].0 } /// Getter for the lowest frequency that is captured inside this spectrum. @@ -218,10 +243,10 @@ impl FrequencySpectrum { /// This corresponds to the [`crate::limit::FrequencyLimit`] of the spectrum. /// /// This method could return the DC component, see [`Self::dc_component`]. - #[inline(always)] + #[inline] + #[must_use] pub fn min_fr(&self) -> Frequency { - let data = self.data.borrow(); - data[0].0 + self.data[0].0 } /// Returns the *DC Component* or also called *DC bias* which corresponds @@ -238,10 +263,10 @@ impl FrequencySpectrum { /// tend to filter out any DC component at the analogue level. In cases where you might /// be interested it can be calculated directly as an average in the usual way, without /// resorting to a DFT/FFT.* - Paul R. - #[inline(always)] + #[inline] + #[must_use] pub fn dc_component(&self) -> Option { - let data = self.data.borrow(); - let (maybe_dc_component, dc_value) = &data[0]; + let (maybe_dc_component, dc_value) = &self.data[0]; if maybe_dc_component.val() == 0.0 { Some(*dc_value) } else { @@ -267,15 +292,13 @@ impl FrequencySpectrum { /// /// ## Return /// Either exact value of approximated value, determined by [`Self::frequency_resolution`]. - #[inline(always)] + #[inline] + #[must_use] pub fn freq_val_exact(&self, search_fr: f32) -> FrequencyValue { - let data = self.data.borrow(); - // lowest frequency in the spectrum - // TODO use minFrequency() and maxFrequency() - let (min_fr, min_fr_val) = data[0]; + let (min_fr, min_fr_val) = self.data[0]; // highest frequency in the spectrum - let (max_fr, max_fr_val) = data[data.len() - 1]; + let (max_fr, max_fr_val) = self.data[self.data.len() - 1]; // https://docs.rs/float-cmp/0.8.0/float_cmp/ let equals_min_fr = float_cmp::approx_eq!(f32, min_fr.val(), search_fr, ulps = 3); @@ -301,7 +324,7 @@ impl FrequencySpectrum { // We search for Point C (x=search_fr, y=???) between Point A and Point B iteratively. // Point B is always the successor of A. - for two_points in data.iter().as_slice().windows(2) { + for two_points in self.data.iter().as_slice().windows(2) { let point_a = two_points[0]; let point_b = two_points[1]; let point_a_x = point_a.0.val(); @@ -349,15 +372,13 @@ impl FrequencySpectrum { /// /// ## Return /// Closest matching point in spectrum, determined by [`Self::frequency_resolution`]. - #[inline(always)] + #[inline] + #[must_use] pub fn freq_val_closest(&self, search_fr: f32) -> (Frequency, FrequencyValue) { - let data = self.data.borrow(); - // lowest frequency in the spectrum - // TODO use minFrequency() and maxFrequency() - let (min_fr, min_fr_val) = data[0]; + let (min_fr, min_fr_val) = self.data[0]; // highest frequency in the spectrum - let (max_fr, max_fr_val) = data[data.len() - 1]; + let (max_fr, max_fr_val) = self.data[self.data.len() - 1]; // https://docs.rs/float-cmp/0.8.0/float_cmp/ let equals_min_fr = float_cmp::approx_eq!(f32, min_fr.val(), search_fr, ulps = 3); @@ -381,7 +402,7 @@ impl FrequencySpectrum { ); } - for two_points in data.iter().as_slice().windows(2) { + for two_points in self.data.iter().as_slice().windows(2) { let point_a = two_points[0]; let point_b = two_points[1]; let point_a_x = point_a.0; @@ -413,72 +434,108 @@ impl FrequencySpectrum { panic!("Here be dragons"); } - /// Returns a `BTreeMap`. The key is of type u32. - /// (`f32` is not `Ord`, hence we can't use it as key.) You can optionally specify a - /// scale function, e.g. multiply all frequencies with 1000 for better - /// accuracy when represented as unsigned integer. + /// Wrapper around [`Self::freq_val_exact`] that consumes [mel]. /// - /// ## Parameters - /// * `scale_fn` optional scale function, e.g. multiply all frequencies with 1000 for better - /// accuracy when represented as unsigned integer. + /// [mel]: https://en.wikipedia.org/wiki/Mel_scale + #[inline] + #[must_use] + pub fn mel_val(&self, mel_val: f32) -> FrequencyValue { + let hz = mel_to_hertz(mel_val); + self.freq_val_exact(hz) + } + + /// Returns a [`BTreeMap`] with all value pairs. The key is of type [`u32`] + /// because [`f32`] is not [`Ord`]. + #[inline] + #[must_use] + pub fn to_map(&self) -> BTreeMap { + self.data + .iter() + .map(|(fr, fr_val)| (fr.val() as u32, fr_val.val())) + .collect() + } + + /// Like [`Self::to_map`] but converts the frequency (x-axis) to [mels]. The + /// resulting map contains more results in a higher density the higher the + /// mel value gets. This comes from the logarithmic transformation from + /// hertz to mels. /// - /// ## Return - /// New `BTreeMap` from frequency to frequency value. - #[inline(always)] - pub fn to_map(&self, scale_fn: Option<&dyn Fn(f32) -> u32>) -> BTreeMap { + /// [mels]: https://en.wikipedia.org/wiki/Mel_scale + #[inline] + #[must_use] + pub fn to_mel_map(&self) -> BTreeMap { self.data - .borrow() .iter() - .map(|(fr, fr_val)| (fr.val(), fr_val.val())) - .map(|(fr, fr_val)| (scale_fn.map_or(fr as u32, |fnc| (fnc)(fr)), fr_val)) + .map(|(fr, fr_val)| (hertz_to_mel(fr.val()) as u32, fr_val.val())) .collect() } - /// Calculates min, max, median and average of the frequency values/magnitudes/amplitudes. - #[inline(always)] - fn calc_statistics(&self) { - let mut data_sorted = self.data.borrow().clone(); - data_sorted.sort_by(|(_l_fr, l_fr_val), (_r_fr, r_fr_val)| { - // compare by frequency value, from min to max - l_fr_val.cmp(r_fr_val) - }); + /// Calculates the `min`, `max`, `median`, and `average` of the frequency values/magnitudes/ + /// amplitudes. + /// + /// To do so, it needs to create a sorted copy of the data. + #[inline] + fn calc_statistics(&mut self, working_buffer: &mut [(Frequency, FrequencyValue)]) { + // We create a copy with all data from `self.data` but we sort it by the + // frequency value and not the frequency. This way, we can easily find the + // median. + + let data_sorted_by_val = { + assert_eq!( + self.data.len(), + working_buffer.len(), + "The working buffer must have the same length as `self.data`!" + ); + + for (i, pair) in self.data.iter().enumerate() { + working_buffer[i] = *pair; + } + working_buffer.sort_by(|(_l_fr, l_fr_val), (_r_fr, r_fr_val)| { + // compare by frequency value, from min to max + l_fr_val.cmp(r_fr_val) + }); + + working_buffer + }; - // sum - let sum: f32 = data_sorted + // sum of all frequency values + let sum: f32 = data_sorted_by_val .iter() .map(|fr_val| fr_val.1.val()) .fold(0.0, |a, b| a + b); - let avg = sum / data_sorted.len() as f32; + // average of all frequency values + let avg = sum / data_sorted_by_val.len() as f32; let average: FrequencyValue = avg.into(); + // median of all frequency values let median = { - // we assume that data_sorted.length() is always even, because + // we assume that data_sorted_by_val.length() is always even, because // it must be a power of 2 (for FFT) - let a = data_sorted[data_sorted.len() / 2 - 1].1; - let b = data_sorted[data_sorted.len() / 2].1; + let a = data_sorted_by_val[data_sorted_by_val.len() / 2 - 1].1; + let b = data_sorted_by_val[data_sorted_by_val.len() / 2].1; (a + b) / 2.0.into() }; - // because we sorted the vector a few lines above - // by the value, the following lines are correct - // i.e. we get min/max value with corresponding frequency - let min = data_sorted[0]; - let max = data_sorted[data_sorted.len() - 1]; + // Because we sorted the vector from lowest to highest value, the + // following lines are correct, i.e., we get min/max value with + // the corresponding frequency. + let min = data_sorted_by_val[0]; + let max = data_sorted_by_val[data_sorted_by_val.len() - 1]; // check that I get the comparison right (and not from max to min) debug_assert!(min.1 <= max.1, "min must be <= max"); - self.min.replace(min); - self.max.replace(max); - self.average.replace(average); - self.median.replace(median); + self.min = min; + self.max = max; + self.average = average; + self.median = median; } } /*impl FromIterator<(Frequency, FrequencyValue)> for FrequencySpectrum { - #[inline(always)] + #[inline] fn from_iter>(iter: T) -> Self { // 1024 is just a guess: most likely 2048 is a common FFT length, // i.e. 1024 results for the frequency spectrum. @@ -491,67 +548,105 @@ impl FrequencySpectrum { } }*/ -/// Calculates the y coordinate of Point C between two given points A and B -/// if the x-coordinate of C is known. It does that by putting a linear function -/// through the two given points. -/// -/// ## Parameters -/// - `(x1, y1)` x and y of point A -/// - `(x2, y2)` x and y of point B -/// - `x_coord` x coordinate of searched point C -/// -/// ## Return Value -/// y coordinate of searched point C -#[inline(always)] -fn calculate_y_coord_between_points( - (x1, y1): (f32, f32), - (x2, y2): (f32, f32), - x_coord: f32, -) -> f32 { - // e.g. Points (100, 1.0) and (200, 0.0) - // y=f(x)=-0.01x + c - // 1.0 = f(100) = -0.01x + c - // c = 1.0 + 0.01*100 = 2.0 - // y=f(180)=-0.01*180 + 2.0 - - // gradient, anstieg - let slope = (y2 - y1) / (x2 - x1); - // calculate c in y=f(x)=slope * x + c - let c = y1 - slope * x1; - - slope * x_coord + c -} +mod math { + // use super::*; -#[cfg(test)] -mod tests { - use super::*; + /// Calculates the y coordinate of Point C between two given points A and B + /// if the x-coordinate of C is known. It does that by putting a linear function + /// through the two given points. + /// + /// ## Parameters + /// - `(x1, y1)` x and y of point A + /// - `(x2, y2)` x and y of point B + /// - `x_coord` x coordinate of searched point C + /// + /// ## Return Value + /// y coordinate of searched point C + #[inline] + pub fn calculate_y_coord_between_points( + (x1, y1): (f32, f32), + (x2, y2): (f32, f32), + x_coord: f32, + ) -> f32 { + // e.g. Points (100, 1.0) and (200, 0.0) + // y=f(x)=-0.01x + c + // 1.0 = f(100) = -0.01x + c + // c = 1.0 + 0.01*100 = 2.0 + // y=f(180)=-0.01*180 + 2.0 + + // gradient, anstieg + let slope = (y2 - y1) / (x2 - x1); + // calculate c in y=f(x)=slope * x + c + let c = y1 - slope * x1; + + slope * x_coord + c + } - #[test] - fn test_calculate_y_coord_between_points() { - assert_eq!( - // expected y coordinate - 0.5, - calculate_y_coord_between_points( - (100.0, 1.0), - (200.0, 0.0), - 150.0, - ), - "Must calculate middle point between points by laying a linear function through the two points" - ); - assert!( - // https://docs.rs/float-cmp/0.8.0/float_cmp/ - float_cmp::approx_eq!( - f32, - 0.2, + /// Converts hertz to [mel](https://en.wikipedia.org/wiki/Mel_scale). + pub fn hertz_to_mel(hz: f32) -> f32 { + assert!(hz >= 0.0); + 2595.0 * libm::log10f(1.0 + (hz / 700.0)) + } + + /// Converts [mel](https://en.wikipedia.org/wiki/Mel_scale) to hertz. + pub fn mel_to_hertz(mel: f32) -> f32 { + assert!(mel >= 0.0); + 700.0 * (libm::powf(10.0, mel / 2595.0) - 1.0) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_calculate_y_coord_between_points() { + assert_eq!( + // expected y coordinate + 0.5, calculate_y_coord_between_points( (100.0, 1.0), (200.0, 0.0), - 180.0, + 150.0, ), + "Must calculate middle point between points by laying a linear function through the two points" + ); + // Must calculate arbitrary point between points by laying a linear function through the + // two points. + float_cmp::assert_approx_eq!( + f32, + 0.2, + calculate_y_coord_between_points((100.0, 1.0), (200.0, 0.0), 180.0,), ulps = 3 - ), - "Must calculate arbitrary point between points by laying a linear function through the two points" - ); + ); + } + + #[test] + fn test_mel() { + float_cmp::assert_approx_eq!(f32, hertz_to_mel(0.0), 0.0, epsilon = 0.1); + float_cmp::assert_approx_eq!(f32, hertz_to_mel(500.0), 607.4, epsilon = 0.1); + float_cmp::assert_approx_eq!(f32, hertz_to_mel(5000.0), 2363.5, epsilon = 0.1); + + let conv = |hz: f32| mel_to_hertz(hertz_to_mel(hz)); + + float_cmp::assert_approx_eq!(f32, conv(0.0), 0.0, epsilon = 0.1); + float_cmp::assert_approx_eq!(f32, conv(1000.0), 1000.0, epsilon = 0.1); + float_cmp::assert_approx_eq!(f32, conv(10000.0), 10000.0, epsilon = 0.1); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test if a frequency spectrum can be sent to other threads. + #[test] + const fn test_impl_send() { + #[allow(unused)] + // test if this compiles + fn consume(s: FrequencySpectrum) { + let _: &dyn Send = &s; + } } #[test] @@ -566,14 +661,20 @@ mod tests { (250.0, 20.0), (300.0, 0.0), (450.0, 200.0), + (500.0, 100.0), ]; - let spectrum = spectrum + let mut spectrum_vector = spectrum .into_iter() .map(|(fr, val)| (fr.into(), val.into())) .collect::>(); - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); // test inner vector is ordered { @@ -617,23 +718,24 @@ mod tests { spectrum.data()[7], "Vector must be ordered" ); + assert_eq!( + (500.0.into(), 100.0.into()), + spectrum.data()[8], + "Vector must be ordered" + ); } // test DC component getter - assert!( - spectrum.dc_component().is_some(), - "Spectrum must contain DC component" - ); assert_eq!( - 5.0, - spectrum.dc_component().unwrap().val(), + Some(5.0.into()), + spectrum.dc_component(), "Spectrum must contain DC component" ); // test getters { assert_eq!(0.0, spectrum.min_fr().val(), "min_fr() must work"); - assert_eq!(450.0, spectrum.max_fr().val(), "max_fr() must work"); + assert_eq!(500.0, spectrum.max_fr().val(), "max_fr() must work"); assert_eq!( (300.0.into(), 0.0.into()), spectrum.min(), @@ -645,7 +747,7 @@ mod tests { "max() must work" ); assert_eq!(200.0 - 0.0, spectrum.range().val(), "range() must work"); - assert_eq!(78.125, spectrum.average().val(), "average() must work"); + assert_eq!(80.55556, spectrum.average().val(), "average() must work"); assert_eq!( (50 + 100) as f32 / 2.0, spectrum.median().val(), @@ -694,14 +796,17 @@ mod tests { #[test] #[should_panic] fn test_spectrum_get_frequency_value_exact_panic_below_min() { - let spectrum_vector = vec![(0.0_f32, 5.0_f32), (450.0, 200.0)]; + let mut spectrum_vector = vec![ + (0.0_f32.into(), 5.0_f32.into()), + (450.0.into(), 200.0.into()), + ]; - let spectrum = spectrum_vector - .into_iter() - .map(|(fr, val)| (fr.into(), val.into())) - .collect::>(); - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); // -1 not included, expect panic spectrum.freq_val_exact(-1.0).val(); @@ -710,14 +815,17 @@ mod tests { #[test] #[should_panic] fn test_spectrum_get_frequency_value_exact_panic_below_max() { - let spectrum_vector = vec![(0.0_f32, 5.0_f32), (450.0, 200.0)]; + let mut spectrum_vector = vec![ + (0.0_f32.into(), 5.0_f32.into()), + (450.0.into(), 200.0.into()), + ]; - let spectrum = spectrum_vector - .into_iter() - .map(|(fr, val)| (fr.into(), val.into())) - .collect::>(); - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); // 451 not included, expect panic spectrum.freq_val_exact(451.0).val(); @@ -726,45 +834,51 @@ mod tests { #[test] #[should_panic] fn test_spectrum_get_frequency_value_closest_panic_below_min() { - let spectrum_vector = vec![(0.0_f32, 5.0_f32), (450.0, 200.0)]; - - let spectrum = spectrum_vector - .into_iter() - .map(|(fr, val)| (fr.into(), val.into())) - .collect::>(); - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + let mut spectrum_vector = vec![ + (0.0_f32.into(), 5.0_f32.into()), + (450.0.into(), 200.0.into()), + ]; + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); // -1 not included, expect panic - spectrum.freq_val_closest(-1.0); + let _ = spectrum.freq_val_closest(-1.0); } #[test] #[should_panic] fn test_spectrum_get_frequency_value_closest_panic_below_max() { - let spectrum_vector = vec![(0.0_f32, 5.0_f32), (450.0, 200.0)]; + let mut spectrum_vector = vec![ + (0.0_f32.into(), 5.0_f32.into()), + (450.0.into(), 200.0.into()), + ]; - let spectrum = spectrum_vector - .into_iter() - .map(|(fr, val)| (fr.into(), val.into())) - .collect::>(); - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); // 451 not included, expect panic - spectrum.freq_val_closest(451.0); + let _ = spectrum.freq_val_closest(451.0); } #[test] fn test_nan_safety() { - let spectrum_vector: Vec<(Frequency, FrequencyValue)> = vec![(0.0.into(), 0.0.into()); 8]; + let mut spectrum_vector: Vec<(Frequency, FrequencyValue)> = + vec![(0.0.into(), 0.0.into()); 8]; - let spectrum_len = spectrum_vector.len() as u32; let spectrum = FrequencySpectrum::new( - spectrum_vector, - // not important here, any valu + spectrum_vector.clone(), + // not important here, any value 50.0, - spectrum_len, + spectrum_vector.len() as _, + &mut spectrum_vector, ); assert_ne!( @@ -812,15 +926,75 @@ mod tests { #[test] fn test_no_dc_component() { - let spectrum: Vec<(Frequency, FrequencyValue)> = + let mut spectrum_vector: Vec<(Frequency, FrequencyValue)> = vec![(150.0.into(), 150.0.into()), (200.0.into(), 100.0.into())]; - let spectrum_len = spectrum.len() as u32; - let spectrum = FrequencySpectrum::new(spectrum, 50.0, spectrum_len); + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); assert!( spectrum.dc_component().is_none(), "This spectrum should not contain a DC component!" ) } + + #[test] + fn test_max() { + let maximum: (Frequency, FrequencyValue) = (34.991455.into(), 86.791145.into()); + let mut spectrum_vector: Vec<(Frequency, FrequencyValue)> = vec![ + (2.6916504.into(), 22.81816.into()), + (5.383301.into(), 2.1004658.into()), + (8.074951.into(), 8.704016.into()), + (10.766602.into(), 3.4043686.into()), + (13.458252.into(), 8.649045.into()), + (16.149902.into(), 9.210494.into()), + (18.841553.into(), 14.937911.into()), + (21.533203.into(), 5.1524887.into()), + (24.224854.into(), 20.706167.into()), + (26.916504.into(), 8.359295.into()), + (29.608154.into(), 3.7514696.into()), + (32.299805.into(), 15.109907.into()), + maximum, + (37.683105.into(), 52.140736.into()), + (40.374756.into(), 24.108875.into()), + (43.066406.into(), 11.070151.into()), + (45.758057.into(), 10.569871.into()), + (48.449707.into(), 6.1969466.into()), + (51.141357.into(), 16.722788.into()), + (53.833008.into(), 8.93011.into()), + ]; + + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 44100.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); + + assert_eq!( + spectrum.max(), + maximum, + "Should return the maximum frequency value!" + ) + } + + #[test] + fn test_mel_getter() { + let mut spectrum_vector = vec![ + (0.0_f32.into(), 5.0_f32.into()), + (450.0.into(), 200.0.into()), + ]; + + let spectrum = FrequencySpectrum::new( + spectrum_vector.clone(), + 50.0, + spectrum_vector.len() as _, + &mut spectrum_vector, + ); + let _ = spectrum.mel_val(450.0); + } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f43204b..ddaf43f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -95,21 +95,21 @@ fn test_spectrum_and_visualize_sine_waves_50_1000_3777hz() { spectrum_static_plotters_png_visualize( // spectrum_static_png_visualize( - &spectrum_no_window.to_map(None), + &spectrum_no_window.to_map(), TEST_OUT_DIR, "test_spectrum_and_visualize_sine_waves_50_1000_3777hz--no-window.png", ); spectrum_static_plotters_png_visualize( // spectrum_static_png_visualize( - &spectrum_hamming_window.to_map(None), + &spectrum_hamming_window.to_map(), TEST_OUT_DIR, "test_spectrum_and_visualize_sine_waves_50_1000_3777hz--hamming-window.png", ); spectrum_static_plotters_png_visualize( // spectrum_static_png_visualize( - &spectrum_hann_window.to_map(None), + &spectrum_hann_window.to_map(), TEST_OUT_DIR, "test_spectrum_and_visualize_sine_waves_50_1000_3777hz--hann-window.png", ); @@ -179,17 +179,17 @@ fn test_spectrum_power() { .unwrap();*/ spectrum_static_plotters_png_visualize( - &spectrum_short_window.to_map(None), + &spectrum_short_window.to_map(), TEST_OUT_DIR, "test_spectrum_power__short_window.png", ); spectrum_static_plotters_png_visualize( - &spectrum_long_window.to_map(None), + &spectrum_long_window.to_map(), TEST_OUT_DIR, "test_spectrum_power__long_window.png", ); /*spectrum_static_plotters_png_visualize( - &spectrum_long_window.to_map(None), + &spectrum_long_window.to_map(), TEST_OUT_DIR, "test_spectrum_power__very_long_window.png", );*/ diff --git a/src/tests/sine.rs b/src/tests/sine.rs index b153bea..6e67a5e 100644 --- a/src/tests/sine.rs +++ b/src/tests/sine.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 diff --git a/src/windows.rs b/src/windows.rs index d58dcf5..1c10431 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2021 Philipp Schuster +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 @@ -31,20 +31,12 @@ use core::f32::consts::PI; // replacement for std functions like sin and cos in no_std-environments use libm::cosf; -/*/// Describes what window function should be applied to -/// the `samples` parameter of [`crate::samples_fft_to_spectrum`] -/// should be applied before the FFT starts. See -/// https://en.wikipedia.org/wiki/Window_function for more -/// resources. -pub enum WindowFn { - -}*/ - /// Applies a Hann window () /// to an array of samples. /// /// ## Return value /// New vector with Hann window applied to the values. +#[must_use] pub fn hann_window(samples: &[f32]) -> Vec { let mut windowed_samples = Vec::with_capacity(samples.len()); let samples_len_f32 = samples.len() as f32; @@ -62,6 +54,7 @@ pub fn hann_window(samples: &[f32]) -> Vec { /// /// ## Return value /// New vector with Hann window applied to the values. +#[must_use] pub fn hamming_window(samples: &[f32]) -> Vec { let mut windowed_samples = Vec::with_capacity(samples.len()); let samples_len_f32 = samples.len() as f32; @@ -77,6 +70,7 @@ pub fn hamming_window(samples: &[f32]) -> Vec { /// /// ## Return value /// New vector with Blackman-Harris 4-term window applied to the values. +#[must_use] pub fn blackman_harris_4term(samples: &[f32]) -> Vec { // constants come from here: // https://en.wikipedia.org/wiki/Window_function#Blackman%E2%80%93Harris_window @@ -94,6 +88,7 @@ pub fn blackman_harris_4term(samples: &[f32]) -> Vec { /// /// ## Return value /// New vector with Blackman-Harris 7-term window applied to the values. +#[must_use] pub fn blackman_harris_7term(samples: &[f32]) -> Vec { // constants come from here: // https://dsp.stackexchange.com/questions/51095/seven-term-blackman-harris-window @@ -116,6 +111,7 @@ pub fn blackman_harris_7term(samples: &[f32]) -> Vec { /// /// ## Return value /// New vector with Blackman-Harris x-term window applied to the values. +#[must_use] fn blackman_harris_xterm(samples: &[f32], alphas: &[f32]) -> Vec { let mut windowed_samples = Vec::with_capacity(samples.len());