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