spectrum_analyzer/
lib.rs

1/*
2MIT License
3
4Copyright (c) 2023 Philipp Schuster
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24//! An easy to use and fast `no_std` library (with `alloc`) to get the frequency
25//! spectrum of a digital signal (e.g. audio) using FFT.
26//!
27//! ## Examples
28//! ### Scaling via dynamic closure
29//! ```rust
30//! use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
31//! // get data from audio source
32//! let samples = vec![0.0, 1.1, 5.5, -5.5];
33//! let res = samples_fft_to_spectrum(
34//!         &samples,
35//!         44100,
36//!         FrequencyLimit::All,
37//!         Some(&|val, info| val - info.min),
38//! );
39//! ```
40//! ### Scaling via static function
41//! ```rust
42//! use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
43//! use spectrum_analyzer::scaling::divide_by_N_sqrt;
44//! // get data from audio source
45//! let samples = vec![0.0, 1.1, 5.5, -5.5];
46//! let res = samples_fft_to_spectrum(
47//!         &samples,
48//!         44100,
49//!         FrequencyLimit::All,
50//!         // Recommended scaling/normalization by `rustfft`.
51//!         Some(&divide_by_N_sqrt),
52//! );
53//! ```
54
55#![deny(
56    clippy::all,
57    clippy::cargo,
58    clippy::nursery,
59    clippy::must_use_candidate
60    // clippy::restriction,
61    // clippy::pedantic
62)]
63// now allow a few rules which are denied by the above statement
64// --> they are ridiculous and not necessary
65#![allow(
66    clippy::suboptimal_flops,
67    clippy::redundant_pub_crate,
68    clippy::fallible_impl_from,
69    clippy::float_cmp
70)]
71#![deny(missing_docs)]
72#![deny(missing_debug_implementations)]
73#![deny(rustdoc::all)]
74#![no_std]
75
76#[cfg_attr(test, macro_use)]
77#[cfg(test)]
78extern crate std;
79
80#[macro_use]
81extern crate alloc;
82
83pub use crate::frequency::{Frequency, FrequencyValue};
84pub use crate::limit::FrequencyLimit;
85pub use crate::limit::FrequencyLimitError;
86pub use crate::spectrum::FrequencySpectrum;
87
88use crate::error::SpectrumAnalyzerError;
89use crate::fft::{Complex32, FftImpl};
90use crate::scaling::SpectrumScalingFunction;
91use alloc::vec::Vec;
92
93pub mod error;
94mod fft;
95mod frequency;
96mod limit;
97pub mod scaling;
98mod spectrum;
99pub mod windows;
100
101// test module for large "integration"-like tests
102#[cfg(test)]
103mod tests;
104
105/// Takes an array of samples (length must be a power of 2),
106/// e.g. 2048, applies an FFT (using the specified FFT implementation) on it
107/// and returns all frequencies with their volume/magnitude.
108///
109/// By default, no normalization/scaling is done at all and the results,
110/// i.e. the frequency magnitudes/amplitudes/values are the raw result from
111/// the FFT algorithm, except that complex numbers are transformed
112/// to their magnitude.
113///
114/// * `samples` raw audio, e.g. 16bit audio data but as f32.
115///   You should apply a window function (like Hann) on the data first.
116///   The final frequency resolution is `sample_rate / (N / 2)`
117///   e.g. `44100/(16384/2) == 5.383Hz`, i.e. more samples =>
118///   better accuracy/frequency resolution. The amount of samples must
119///   be a power of 2. If you don't have enough data, provide zeroes.
120/// * `sampling_rate` The used sampling_rate, e.g. `44100 [Hz]`.
121/// * `frequency_limit` The [`FrequencyLimit`].
122/// * `scaling_fn` See [`SpectrumScalingFunction`] for details.
123///
124/// ## Returns value
125/// New object of type [`FrequencySpectrum`].
126///
127/// ## Examples
128/// ### Scaling via dynamic closure
129/// ```rust
130/// use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
131/// // get data from audio source
132/// let samples = vec![0.0, 1.1, 5.5, -5.5];
133/// let res = samples_fft_to_spectrum(
134///         &samples,
135///         44100,
136///         FrequencyLimit::All,
137///         Some(&|val, info| val - info.min),
138///  );
139/// ```
140/// ### Scaling via static function
141/// ```rust
142/// use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
143/// use spectrum_analyzer::scaling::scale_to_zero_to_one;
144/// // get data from audio source
145/// let samples = vec![0.0, 1.1, 5.5, -5.5];
146/// let res = samples_fft_to_spectrum(
147///         &samples,
148///         44100,
149///         FrequencyLimit::All,
150///         Some(&scale_to_zero_to_one),
151///  );
152/// ```
153///
154/// ## Panics
155/// * When `samples.len()` isn't a power of two less than or equal to `16384` and `microfft` is used
156pub fn samples_fft_to_spectrum(
157    samples: &[f32],
158    sampling_rate: u32,
159    frequency_limit: FrequencyLimit,
160    scaling_fn: Option<&SpectrumScalingFunction>,
161) -> Result<FrequencySpectrum, SpectrumAnalyzerError> {
162    // everything below two samples is unreasonable
163    if samples.len() < 2 {
164        return Err(SpectrumAnalyzerError::TooFewSamples);
165    }
166    // do several checks on input data
167    if samples.iter().any(|x| x.is_nan()) {
168        return Err(SpectrumAnalyzerError::NaNValuesNotSupported);
169    }
170    if samples.iter().any(|x| x.is_infinite()) {
171        return Err(SpectrumAnalyzerError::InfinityValuesNotSupported);
172    }
173    if !samples.len().is_power_of_two() {
174        return Err(SpectrumAnalyzerError::SamplesLengthNotAPowerOfTwo);
175    }
176    let max_detectable_frequency = sampling_rate as f32 / 2.0;
177    // verify frequency limit: unwrap error or else ok
178    frequency_limit
179        .verify(max_detectable_frequency)
180        .map_err(SpectrumAnalyzerError::InvalidFrequencyLimit)?;
181
182    // With FFT we transform an array of time-domain waveform samples
183    // into an array of frequency-domain spectrum samples
184    // https://www.youtube.com/watch?v=z7X6jgFnB6Y
185
186    // FFT result has same length as input
187    // (but when we interpret the result, we don't need all indices)
188
189    // applies the f32 samples onto the FFT algorithm implementation
190    // chosen at compile time (via Cargo feature).
191    // If a complex FFT implementation was chosen, this will internally
192    // transform all data to Complex numbers.
193    let fft_res = FftImpl::calc(samples);
194
195    // This function:
196    // 1) calculates the corresponding frequency of each index in the FFT result
197    // 2) filters out unwanted frequencies
198    // 3) calculates the magnitude (absolute value) at each frequency index for each complex value
199    // 4) optionally scales the magnitudes
200    // 5) collects everything into the struct "FrequencySpectrum"
201    fft_result_to_spectrum(
202        samples.len(),
203        &fft_res,
204        sampling_rate,
205        frequency_limit,
206        scaling_fn,
207    )
208}
209
210/// Transforms the FFT result into the spectrum by calculating the corresponding frequency of each
211/// FFT result index and optionally calculating the magnitudes of the complex numbers if a complex
212/// FFT implementation is chosen.
213///
214/// ## Parameters
215/// * `samples_len` Length of samples. This is a dedicated field because it can't always be
216///   derived from `fft_result.len()`. There are for example differences for
217///   `fft_result.len()` in real and complex FFT algorithms.
218/// * `fft_result` Result buffer from FFT. Has the same length as the samples array.
219/// * `sampling_rate` The used sampling_rate, e.g. `44100 [Hz]`.
220/// * `frequency_limit` The [`FrequencyLimit`].
221/// * `scaling_fn` See [`SpectrumScalingFunction`] for details.
222///
223/// ## Return value
224/// New object of type [`FrequencySpectrum`].
225#[inline]
226fn fft_result_to_spectrum(
227    samples_len: usize,
228    fft_result: &[Complex32],
229    sampling_rate: u32,
230    frequency_limit: FrequencyLimit,
231    scaling_fn: Option<&SpectrumScalingFunction>,
232) -> Result<FrequencySpectrum, SpectrumAnalyzerError> {
233    let maybe_min = frequency_limit.maybe_min();
234    let maybe_max = frequency_limit.maybe_max();
235
236    let frequency_resolution = fft_calc_frequency_resolution(sampling_rate, samples_len as u32);
237
238    // collect frequency => frequency value in Vector of Pairs/Tuples
239    let frequency_vec = fft_result
240        .iter()
241        // See https://stackoverflow.com/a/4371627/2891595 for more information as well as
242        // https://www.gaussianwaves.com/2015/11/interpreting-fft-results-complex-dft-frequency-bins-and-fftshift/
243        //
244        // The indices 0 to N/2 (inclusive) are usually the most relevant. Although, index
245        // N/2-1 is declared as the last useful one on stackoverflow (because in typical applications
246        // Nyquist-frequency + above are filtered out), we include everything here.
247        // with 0..=(samples_len / 2) (inclusive) we get all frequencies from 0 to Nyquist theorem.
248        //
249        // Indices (samples_len / 2)..len() are mirrored/negative. You can also see this here:
250        // https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2015/11/realDFT_complexDFT.png
251        .take(samples_len / 2 + 1)
252        // to (index, fft-result)-pairs
253        .enumerate()
254        // calc index => corresponding frequency
255        .map(|(fft_index, fft_result)| {
256            (
257                // Calculate corresponding frequency of each index of FFT result.
258                //
259                // Explanation for the algorithm:
260                // https://stackoverflow.com/questions/4364823/
261                //
262                // N complex samples          : [0], [1], [2], [3], ... , ..., [2047] => 2048 samples for example
263                //   (Or N real samples packed
264                //   into N/2 complex samples
265                //   (real FFT algorithm))
266                // Complex FFT Result         : [0], [1], [2], [3], ... , ..., [2047]
267                // Relevant part of FFT Result: [0], [1], [2], [3], ... , [1024]      => indices 0 to N/2 (inclusive) are important
268                //                               ^                         ^
269                // Frequency                  : 0Hz, .................... Sampling Rate/2 => "Nyquist frequency"
270                //                              0Hz is also called        (e.g. 22050Hz for 44100Hz sampling rate)
271                //                              "DC Component"
272                //
273                // frequency step/resolution is for example: 1/2048 * 44100 = 21.53 Hz
274                //                                             2048 samples, 44100 sample rate
275                //
276                // equal to: 1.0 / samples_len as f32 * sampling_rate as f32
277                fft_index as f32 * frequency_resolution,
278                // in this .map() step we do nothing with this yet
279                fft_result,
280            )
281        })
282        // #######################
283        // ### BEGIN filtering: results in lower calculation and memory overhead!
284        // check lower bound frequency (inclusive)
285        .filter(|(fr, _fft_result)| {
286            maybe_min.map_or(true, |min_fr| {
287                // inclusive!
288                // attention: due to the frequency resolution, we do not necessarily hit
289                //            exactly the frequency, that a user requested
290                //            e.g. 1416.8 < limit < 1425.15
291                *fr >= min_fr
292            })
293        })
294        // check upper bound frequency (inclusive)
295        .filter(|(fr, _fft_result)| {
296            maybe_max.map_or(true, |max_fr| {
297                // inclusive!
298                // attention: due to the frequency resolution, we do not necessarily hit
299                //            exactly the frequency, that a user requested
300                //            e.g. 1416.8 < limit < 1425.15
301                *fr <= max_fr
302            })
303        })
304        // ### END filtering
305        // #######################
306        // FFT result is always complex: calc magnitude
307        //   sqrt(re*re + im*im) (re: real part, im: imaginary part)
308        .map(|(fr, complex_res)| (fr, complex_to_magnitude(complex_res)))
309        // transform to my thin convenient orderable f32 wrappers
310        .map(|(fr, val)| (Frequency::from(fr), FrequencyValue::from(val)))
311        // collect all into a sorted vector (from lowest frequency to highest)
312        .collect::<Vec<(Frequency, FrequencyValue)>>();
313
314    let mut working_buffer = vec![(0.0.into(), 0.0.into()); frequency_vec.len()];
315
316    // create spectrum object
317    let mut spectrum = FrequencySpectrum::new(
318        frequency_vec,
319        frequency_resolution,
320        samples_len as u32,
321        &mut working_buffer,
322    );
323
324    // optionally scale
325    if let Some(scaling_fn) = scaling_fn {
326        spectrum.apply_scaling_fn(scaling_fn, &mut working_buffer)?
327    }
328
329    Ok(spectrum)
330}
331
332/// Calculate the frequency resolution of the FFT. It is determined by the sampling rate
333/// in Hertz and N, the number of samples given into the FFT. With the frequency resolution,
334/// we can determine the corresponding frequency of each index in the FFT result buffer.
335///
336/// For "real FFT" implementations
337///
338/// ## Parameters
339/// * `samples_len` Number of samples put into the FFT
340/// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]`
341///
342/// ## Return value
343/// Frequency resolution in Hertz.
344///
345/// ## More info
346/// * <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>
347/// * <https://stackoverflow.com/questions/4364823/>
348#[inline]
349fn fft_calc_frequency_resolution(sampling_rate: u32, samples_len: u32) -> f32 {
350    sampling_rate as f32 / samples_len as f32
351}
352
353/// Maps a [`Complex32`] to its magnitude as `f32`. This is done by calculating
354/// `sqrt(re*re + im*im)`. This is required to convert the complex FFT results
355/// back to real values.
356///
357/// ## Parameters
358/// * `val` A single value from the FFT output buffer of type [`Complex32`].
359fn complex_to_magnitude(val: &Complex32) -> f32 {
360    // calculates sqrt(re*re + im*im), i.e. magnitude of complex number
361    let sum = val.re * val.re + val.im * val.im;
362    let sqrt = libm::sqrtf(sum);
363    debug_assert!(!sqrt.is_nan(), "sqrt is NaN!");
364    sqrt
365}