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//! ## Getting started
28//! If you are unsure what to pick, start here. The
29//! [`samples_fft_to_spectrum()`] function is the entry into the library. The
30//! following configuration works for most cases: take a block of samples, apply
31//! a Hann window, and divide the result by the number of samples.
32//!
33//! ```rust
34//! use spectrum_analyzer::scaling::divide_by_N;
35//! use spectrum_analyzer::windows::hann_window;
36//! use spectrum_analyzer::{FrequencyLimit, samples_fft_to_spectrum};
37//!
38//! // your samples; the length must be a power of two
39//! let samples = vec![0.0; 2048];
40//!
41//! let windowed = hann_window(&samples);
42//! let spectrum = samples_fft_to_spectrum(
43//! &windowed,
44//! 44100,
45//! FrequencyLimit::All,
46//! Some(÷_by_N),
47//! )
48//! .unwrap();
49//!
50//! // the loudest frequency in the block
51//! let (frequency, value) = spectrum.max();
52//! ```
53//!
54//! ### How many samples?
55//! More samples mean a finer frequency resolution (`sample_rate / N`), but
56//! they also cover a longer time span, so the spectrum reacts more slowly to
57//! changes. At 44100 Hz, 2048 samples (~46 ms, ~22 Hz per bin) are a good
58//! starting point, 4096 if you need to tell close frequencies apart.
59//!
60//! ### What next?
61//! * [`windows`]: which window function to apply
62//! * [`scaling`]: which scaling to apply
63//! * [`samples_fft_to_spectrum`]: what the resulting values mean
64//! * [`FrequencySpectrum`]: what you can read from the result, e.g.
65//! [`FrequencySpectrum::max`] for the loudest frequency,
66//! [`FrequencySpectrum::freq_val_closest`] for one specific frequency, or
67//! [`FrequencySpectrum::data`] to iterate over all of them
68//!
69//! ## Examples
70//! ### Scaling via dynamic closure
71//! ```rust
72//! use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
73//! // get data from audio source, ideally in range `-1.0..=1.0`
74//! let samples = vec![0.0, 1.1, 5.5, -5.5];
75//! let res = samples_fft_to_spectrum(
76//! &samples,
77//! 44100,
78//! FrequencyLimit::All,
79//! // Create your scaling function as closure on the fly as needed.
80//! Some(&|val, info| val - info.min),
81//! );
82//! ```
83//! ### Scaling via static function
84//! ```rust
85//! use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
86//! use spectrum_analyzer::scaling::divide_by_N;
87//! // get data from audio source, ideally in range `-1.0..=1.0`
88//! let samples = vec![0.0, 1.1, 5.5, -5.5];
89//! let res = samples_fft_to_spectrum(
90//! &samples,
91//! 44100,
92//! FrequencyLimit::All,
93//! // Use one of the provided scaling functions. Here, we make the
94//! // values independent of the number of samples.
95//! Some(÷_by_N),
96//! );
97//! ```
98
99#![deny(
100 clippy::all,
101 clippy::cargo,
102 clippy::nursery,
103 clippy::must_use_candidate,
104 clippy::undocumented_unsafe_blocks,
105 // clippy::restriction,
106 // clippy::pedantic
107)]
108// now allow a few rules which are denied by the above statement
109// --> they are ridiculous and not necessary
110#![allow(
111 clippy::suboptimal_flops,
112 clippy::redundant_pub_crate,
113 clippy::fallible_impl_from,
114 clippy::float_cmp
115)]
116#![deny(missing_docs)]
117#![deny(missing_debug_implementations)]
118#![deny(rustdoc::all)]
119#![no_std]
120
121#[cfg_attr(test, macro_use)]
122#[cfg(test)]
123extern crate std;
124
125// `vec!` is only used in tests; `alloc` itself is used throughout.
126#[cfg_attr(test, macro_use)]
127extern crate alloc;
128
129pub use crate::frequency::{FiniteF32, Frequency, FrequencyValue, NonNegF32};
130pub use crate::limit::FrequencyLimit;
131pub use crate::limit::FrequencyLimitError;
132pub use crate::spectrum::FrequencySpectrum;
133
134use crate::error::SpectrumAnalyzerError;
135use crate::fft::{Complex32, FftImpl};
136use crate::scaling::SpectrumScalingFunction;
137use alloc::vec::Vec;
138
139pub mod error;
140mod fft;
141mod frequency;
142mod limit;
143pub mod scaling;
144mod spectrum;
145pub mod windows;
146
147// test module for large "integration"-like tests
148#[cfg(test)]
149mod tests;
150
151/// Takes an array of samples (length must be a power of 2, such as 2048),
152/// applies an FFT, and returns all frequencies with their magnitude.
153///
154/// If a scaling function was used, the magnitude is scaled/normalized
155/// accordingly.
156///
157/// ## Meaning of the frequency values
158/// Without a scaling function, each value is the plain magnitude of the FFT
159/// result. Think of it as "how much of this frequency is in the samples",
160/// but not in absolute units:
161///
162/// * The values grow with the number of samples: twice the samples, twice
163/// the value.
164/// * A window function (e.g. Hann) shrinks all values by a constant factor,
165/// its coherent gain (see [`windows`]).
166/// * A frequency that falls between two bins reads a bit lower than one that
167/// sits exactly on a bin.
168///
169/// To compare spectra of different lengths, use [`scaling::divide_by_N`].
170/// For the actual amplitude of a sine wave, see the details below.
171///
172/// ### Details
173/// Each value is `sqrt(re*re + im*im)` of the corresponding FFT result,
174/// optionally scaled, and relates to the input as follows:
175///
176/// * A sine wave with amplitude `A` on a bin frequency shows up as
177/// `A * N / 2`, `N` being the number of samples. The DC (0 Hz) and Nyquist
178/// bins show `A * N` instead, because they have no mirror bin.
179/// * A window multiplies each sample by its coefficient before the FFT. The
180/// average coefficient is the coherent gain, e.g. `0.5` for Hann, and every
181/// value in the spectrum shrinks by that factor.
182/// * A frequency between two bins leaks into its neighbors, so its peak reads
183/// lower: up to `36%` lower without a window and `15%` with a Hann window.
184///
185/// So to get the amplitude of a sine wave from a one-sided spectrum: divide by
186/// N and multiply by 2, because the FFT splits the sine wave's amplitude
187/// between its positive- and negative-frequency bins. Do not multiply by 2 for
188/// the DC and Nyquist bins, which have no separate mirror bin. Finally, divide
189/// by the window's coherent gain. This gives the amplitude of the individual
190/// sine wave components that make up the input signal.
191///
192/// ## Parameters
193/// * `samples` Raw audio samples, normalized to `[-1.0; 1.0]`, which is what
194/// audio APIs typically deliver. Other scales work too, as the FFT is
195/// linear and the values simply scale with the input, but the normalized
196/// range keeps the magnitudes small: very large samples can push a
197/// magnitude out of the range of [`f32`].
198/// You should apply a window function (like Hann) on the data first.
199/// The final frequency resolution (spacing between two bins) is
200/// `sample_rate / N`, e.g. `44100/16384 == 2.69Hz`, i.e. more samples =>
201/// better accuracy/frequency resolution. The amount of samples must
202/// be a power of 2. If you don't have enough data, provide zeroes.
203/// * `sampling_rate` The used sampling_rate in Hertz, e.g. `44100`. It must
204/// not be zero, as every frequency of the spectrum derives from it.
205/// * `frequency_limit` The [`FrequencyLimit`].
206/// * `scaling_fn` See [`SpectrumScalingFunction`] for details.
207///
208/// ## Panics
209/// Everything this function can check about its input is reported as an
210/// error. What is left is the magnitude of a frequency leaving the range of
211/// [`f32`], which needs samples far outside the range described above: with
212/// normalized samples, a magnitude never exceeds the number of samples.
213///
214/// ## Examples
215/// ### Scaling via dynamic closure
216/// ```rust
217/// use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
218/// // get data from audio source, ideally in range `-1.0..=1.0`
219/// let samples = vec![0.0, 1.1, 5.5, -5.5];
220/// let res = samples_fft_to_spectrum(
221/// &samples,
222/// 44100,
223/// FrequencyLimit::All,
224/// // Create your scaling function as closure on the fly as needed.
225/// Some(&|val, info| val - info.min),
226/// );
227/// ```
228/// ### Scaling via static function
229/// ```rust
230/// use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
231/// use spectrum_analyzer::scaling::divide_by_N;
232/// // get data from audio source, ideally in range `-1.0..=1.0`
233/// let samples = vec![0.0, 1.1, 5.5, -5.5];
234/// let res = samples_fft_to_spectrum(
235/// &samples,
236/// 44100,
237/// FrequencyLimit::All,
238/// // Use one of the provided scaling functions. Here, we make the
239/// // values independent of the number of samples.
240/// Some(÷_by_N),
241/// );
242pub fn samples_fft_to_spectrum(
243 samples: &[f32],
244 sampling_rate: u32,
245 frequency_limit: FrequencyLimit,
246 scaling_fn: Option<&SpectrumScalingFunction>,
247) -> Result<FrequencySpectrum, SpectrumAnalyzerError> {
248 // do several checks on input data
249 {
250 if samples.len() < 2 || !samples.len().is_power_of_two() || samples.len() > 32768 {
251 return Err(SpectrumAnalyzerError::InvalidLengthOfSamples);
252 }
253 if sampling_rate == 0 {
254 return Err(SpectrumAnalyzerError::InvalidSamplingRate);
255 }
256 let max_detectable_frequency = sampling_rate as f32 / 2.0;
257
258 frequency_limit
259 .verify(max_detectable_frequency)
260 .map_err(SpectrumAnalyzerError::InvalidFrequencyLimit)?;
261
262 for sample in samples {
263 if sample.is_nan() {
264 return Err(SpectrumAnalyzerError::NaNValuesNotSupported);
265 }
266 if sample.is_infinite() {
267 return Err(SpectrumAnalyzerError::InfinityValuesNotSupported);
268 }
269 }
270 }
271
272 // With FFT we transform an array of time-domain waveform samples into an
273 // array of frequency-domain spectrum samples:
274 // https://www.youtube.com/watch?v=z7X6jgFnB6Y
275
276 // The FFT result has same length as input, but when we interpret the
277 // result, we don't need all indices (frequency bins).
278
279 // Applies the FFT on the samples.
280 let fft_res = FftImpl::calc(samples);
281
282 // Process FFT result into a meaningful spectrum.
283 fft_result_to_spectrum(
284 samples.len(),
285 &fft_res,
286 sampling_rate,
287 frequency_limit,
288 scaling_fn,
289 )
290}
291
292/// Transforms the FFT result into a [`FrequencySpectrum`] by calculating the
293/// corresponding frequency of each FFT result (frequency bin) and optionally
294/// scales each value.
295///
296/// ## Parameters
297/// * `samples_len` Number of input samples.
298/// * `fft_result` FFT result, i.e. frequency bins.
299/// * `sampling_rate` Sampling rate of the input samples, e.g. `44100 [Hz]`.
300/// * `frequency_limit` Possibly the bounds of [`FrequencyLimit`] the caller is
301/// interested in.
302/// * `scaling_fn` Optional scaling function to modify each frequency value
303/// (FFT result). See [`SpectrumScalingFunction`] for details.
304#[inline]
305fn fft_result_to_spectrum(
306 samples_len: usize,
307 fft_result: &[Complex32],
308 sampling_rate: u32,
309 frequency_limit: FrequencyLimit,
310 scaling_fn: Option<&SpectrumScalingFunction>,
311) -> Result<FrequencySpectrum, SpectrumAnalyzerError> {
312 let maybe_min = frequency_limit.maybe_min();
313 let maybe_max = frequency_limit.maybe_max();
314
315 let frequency_resolution = fft_calc_frequency_resolution(sampling_rate, samples_len as u32);
316
317 // Number of frequency bins from DC through the Nyquist frequency.
318 let bin_count = samples_len / 2 + 1;
319 debug_assert_eq!(fft_result.len(), bin_count);
320
321 // Preallocate space for the maximum possible number of bins (DC component
322 // up to and including the Nyquist frequency): the filtered iterator below
323 // has no precise size hint, so collecting it directly would grow the
324 // vector with several re-allocations.
325 let mut frequency_vec = Vec::with_capacity(bin_count);
326
327 // frequency => frequency value pairs
328 let bin_iter = fft_result
329 .iter()
330 .enumerate()
331 // Map frequency bin to corresponding frequency (Hz).
332 .map(|(fr_bin, fr_val /* result of the FFT at that index */)| {
333 // Let's assume we have 2048 input samples. A complex FFT produces 2048
334 // complex values. For a real FFT, however, only 1024 complex values are
335 // needed because the negative-frequency half is redundant.
336 //
337 // With a complex FFT, the relevant part of the result would be:
338 //
339 // N real audio samples : [0], [1], [2], [3], ... , [2047] (N = 2048)
340 // ... mapped to ...
341 // N complex audio samples : [0], [1], [2], [3], ... , [2047]
342 // ... put into an FFT ...
343 // Relevant FFT result : [0], [1], [2], [3], ... , [1024]
344 // ^ ^
345 // Frequency : 0 Hz, ..................... Sampling Rate/2
346 // DC component Nyquist frequency
347 // (22050 Hz @ 44100 Hz)
348 //
349 // We use a performance-optimized real FFT with `microfft`. It performs the
350 // calculation in-place: N f32 input values are transformed into N/2 complex
351 // values. The first complex value is special: its real part contains the DC
352 // component, while its imaginary part contains the Nyquist component.
353 //
354 // Thus, the 1024 complex output values contain 1025 frequency values: DC,
355 // bins 1..=1023, and Nyquist. Before we called this, the FFT function
356 // already unpacked the Nyquist component into an additional element of
357 // the FFT result vector that we process here.
358
359 // More information:
360 // - https://stackoverflow.com/questions/4364823/ (explanation of the algorithm)
361 // - https://stackoverflow.com/a/4371627/2891595
362 // - https://www.gaussianwaves.com/2015/11/interpreting-fft-results-complex-dft-frequency-bins-and-fftshift/
363 // - https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2015/11/realDFT_complexDFT.png
364 let fr = fr_bin as f32 * frequency_resolution;
365
366 (fr_bin, fr, fr_val)
367 })
368 // Filter out frequencies we are not interested (lower threshold).
369 .skip_while(|(_fr_bin, fr, _fr_val)| {
370 maybe_min.is_some_and(|min_fr| {
371 // Inclusive!
372 // Attention: due to the frequency resolution, we do not
373 // necessarily hit exactly the frequency, that a user requested
374 // (e.g. 1500 Hz is requested but next matching bin is 1510 Hz).
375 *fr < min_fr
376 })
377 })
378 // Filter out frequencies we are not interested (upper threshold).
379 .take_while(|(_fr_bin, fr, _fr_val)| {
380 maybe_max.is_none_or(|max_fr| {
381 // Inclusive!
382 // Attention: due to the frequency resolution, we do not
383 // necessarily hit exactly the frequency, that a user requested
384 // (e.g. 1500 Hz is requested but next matching bin is 1490 Hz).
385 *fr <= max_fr
386 })
387 })
388 // FFT result is always complex: calc magnitude of complex number to get
389 // the frequency value: sqrt(re*re + im*im) (re: real part, im: imaginary part)
390 .map(|(_fr_bin, fr, fr_val)| {
391 (
392 Frequency::from(fr),
393 FrequencyValue::from(complex_to_magnitude(fr_val)),
394 )
395 });
396
397 // Collect all into a sorted vector (from lowest frequency to highest)
398 frequency_vec.extend(bin_iter);
399 // Give excess memory back if a frequency limit excluded many bins.
400 frequency_vec.shrink_to_fit();
401
402 // A valid frequency limit can still miss all FFT bins, or leave only one.
403 // Statistics and interpolation require at least two frequency points.
404 if frequency_vec.len() < 2 {
405 return Err(SpectrumAnalyzerError::FrequencyLimitTooNarrow);
406 }
407
408 // Create the spectrum wrapper.
409 let mut spectrum =
410 FrequencySpectrum::new(frequency_vec, frequency_resolution, samples_len as u32);
411
412 // Apply the scaling function.
413 if let Some(scaling_fn) = scaling_fn {
414 spectrum.apply_scaling_fn(scaling_fn)?
415 }
416
417 Ok(spectrum)
418}
419
420/// Calculate the frequency resolution of the FFT. It is determined by the
421/// sampling rate in Hertz and N, the number of samples given into the FFT.
422///
423/// With the frequency resolution, we can determine the corresponding frequency
424/// of each index (frequency bin) in the FFT result buffer.
425///
426/// ## Parameters
427/// * `samples_len` Number of samples put into the FFT
428/// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]`
429///
430/// ## Return value
431/// Frequency resolution in Hertz.
432///
433/// ## More info
434/// * <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>
435/// * <https://stackoverflow.com/questions/4364823/>
436#[inline]
437fn fft_calc_frequency_resolution(sampling_rate: u32, samples_len: u32) -> Frequency {
438 Frequency::from(sampling_rate as f32 / samples_len as f32)
439}
440
441/// Maps a [`Complex32`] to its magnitude as `f32`. This is done by calculating
442/// `sqrt(re*re + im*im)`. This is required to convert the complex FFT results
443/// back to real values.
444///
445/// ## Parameters
446/// * `val` A single value from the FFT output buffer of type [`Complex32`].
447///
448/// ## Panics
449/// If the magnitude leaves the range of [`f32`], which needs samples far
450/// outside the range this library expects.
451#[inline]
452fn complex_to_magnitude(val: &Complex32) -> NonNegF32 {
453 // calculates sqrt(re*re + im*im), i.e. magnitude of complex number
454 let sum = val.re * val.re + val.im * val.im;
455 NonNegF32::try_new(libm::sqrtf(sum))
456 .expect("magnitude should be within the range of f32; samples are too large")
457}