Skip to content

Commit

Permalink
Merge pull request #13 from phip1611/dev
Browse files Browse the repository at this point in the history
dev branch for v0.3.0 Release
  • Loading branch information
phip1611 authored Mar 22, 2021
2 parents 4a45965 + e42c0ca commit c30344a
Show file tree
Hide file tree
Showing 10 changed files with 682 additions and 162 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

## v0.3.0
- `FrequencySpectrum::min()` and `FrequencySpectrum::max()`
now return tuples/pairs (issue #6)
- `FrequencySpectrum` now has convenient methods to get
the value of a desired frequency from the underlying vector
of FFT results (issue #8)
- `FrequencySpectrum` now has a field + getter for `frequency_resolution`
(issue #5)

For all issues see: https://github.com/phip1611/spectrum-analyzer/milestone/2
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.
"""
version = "0.2.2"
version = "0.3.0"
authors = ["Philipp Schuster <phip1611@gmail.com>"]
edition = "2018"
keywords = ["fft", "spectrum", "frequencies", "audio", "dsp"]
Expand All @@ -18,6 +18,7 @@ documentation = "https://docs.rs/spectrum-analyzer"

[dependencies]
rustfft = "5.0.1"
float-cmp = "0.8.0" # approx. compare floats

[dev-dependencies]
minimp3 = "0.5.1"
Expand Down
36 changes: 10 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +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.
It follows the KISS principle and consists of simple building blocks/optional features. In short, this is
a convenient wrapper around the great [rustfft](https://crates.io/crates/rustfft)
library.

**I'm not an expert on digital signal processing. Code contributions are highly welcome! :)**

## How to use
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:
```rust
use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
use spectrum_analyzer::windows::hann_window;

fn main() {
// This lib also works in `no_std` environments!
let samples: &[f32] = get_samples(); // TODO implement
let samples: &[f32] = get_samples(); // TODO you need to implement the samples source
// apply hann window for smoothing; length must be a power of 2 for the FFT
let hann_window = hann_window(&samples[0..4096]);
// calc spectrum
Expand All @@ -34,30 +38,10 @@ fn main() {
}
```

### How to scale values
```rust
// e.g. like this
fn get_scale_to_one_fn_factory() -> SpectrumTotalScaleFunctionFactory{
Box::new(
move |min: f32, max: f32, average: f32, median: f32| {
Box::new(
move |x| x/max
)
}
)
}
fn main() {
// ...
let spectrum_hann_window = samples_fft_to_spectrum(
&hann_window,
44100,
FrequencyLimit::All,
None,
// optional total scaling at the end; see doc comments
Some(get_scale_to_one_fn_factory()),
);
}
```
## Scaling the frequency values/amplitudes
As already mentioned, there are lots of comments in the code. Short story is:
Type `ComplexSpectrumScalingFunction` can do anything whereas `BasicSpectrumScalingFunction`
is easier to write, especially for Rust beginners.

## Performance
*Measurements taken on i7-8650U @ 3 Ghz (Single-Core) with optimized build*
Expand Down
18 changes: 6 additions & 12 deletions examples/mp3-samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ use audio_visualizer::spectrum::staticc::plotters_png_file::spectrum_static_plot
use audio_visualizer::test_support::TEST_OUT_DIR;
use minimp3::{Decoder as Mp3Decoder, Error as Mp3Error, Frame as Mp3Frame};
use spectrum_analyzer::windows::{blackman_harris_4term, hamming_window, hann_window};
use spectrum_analyzer::{
samples_fft_to_spectrum, FrequencyLimit, SpectrumTotalScaleFunctionFactory,
};
use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit, scaling};
use std::fs::File;
use std::time::Instant;

Expand Down Expand Up @@ -124,7 +122,7 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
44100,
frequency_limit,
None,
Some(get_scale_to_one_fn_factory()),
Some(scaling::complex::scale_to_zero_to_one()),
);
println!(
"[Measurement]: FFT to Spectrum with no window with {} samples took: {}µs",
Expand All @@ -137,7 +135,7 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
44100,
frequency_limit,
None,
Some(get_scale_to_one_fn_factory()),
Some(scaling::complex::scale_to_zero_to_one()),
);
println!(
"[Measurement]: FFT to Spectrum with Hamming window with {} samples took: {}µs",
Expand All @@ -150,7 +148,7 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
44100,
frequency_limit,
None,
Some(get_scale_to_one_fn_factory()),
Some(scaling::complex::scale_to_zero_to_one()),
);
println!(
"[Measurement]: FFT to Spectrum with Hann window with {} samples took: {}µs",
Expand All @@ -163,7 +161,7 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
44100,
frequency_limit,
None,
Some(get_scale_to_one_fn_factory()),
Some(scaling::complex::scale_to_zero_to_one()),
);
println!("[Measurement]: FFT to Spectrum with Blackmann Harris 4-term window with {} samples took: {}µs", samples.len(), now.elapsed().as_micros());
let now = Instant::now();
Expand All @@ -172,7 +170,7 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
44100,
frequency_limit,
None,
Some(get_scale_to_one_fn_factory()),
Some(scaling::complex::scale_to_zero_to_one()),
);
println!("[Measurement]: FFT to Spectrum with Blackmann Harris 7-term window with {} samples took: {}µs", samples.len(), now.elapsed().as_micros());

Expand Down Expand Up @@ -216,10 +214,6 @@ fn to_spectrum_and_plot(samples: &[f32], filename: &str, frequency_limit: Freque
);
}

fn get_scale_to_one_fn_factory() -> SpectrumTotalScaleFunctionFactory {
Box::new(move |_min: f32, max: f32, _average: f32, _median: f32| Box::new(move |x| x / max))
}

fn read_mp3(file: &str) -> Vec<f32> {
let samples = read_mp3_to_mono(file);
let samples = samples.into_iter().map(|x| x as f32).collect::<Vec<f32>>();
Expand Down
81 changes: 57 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ 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 the great `rustfft` library.

#![no_std]

Expand All @@ -43,15 +45,25 @@ use rustfft::{Fft, FftDirection};

pub use crate::frequency::{Frequency, FrequencyValue};
pub use crate::limit::FrequencyLimit;
pub use crate::spectrum::{FrequencySpectrum, SpectrumTotalScaleFunctionFactory};
pub use crate::spectrum::{FrequencySpectrum, ComplexSpectrumScalingFunction};
use core::convert::identity;

mod frequency;
mod limit;
mod spectrum;
pub mod scaling;
pub mod windows;
#[cfg(test)]
mod tests;
pub mod windows;

/// Definition of a simple function that gets applied on each frequency magnitude
/// in the spectrum. This is easier to write, especially for Rust beginners.
/// Everything that can be achieved with this, can also be achieved with parameter
/// `total_scaling_fn`.
///
/// The scaling only affects the value/amplitude of the frequency
/// but not the frequency itself.
pub type SimpleSpectrumScalingFunction<'a> = &'a dyn Fn(f32) -> f32;

/// Takes an array of samples (length must be a power of 2),
/// e.g. 2048, applies an FFT (using library `rustfft`) on it
Expand All @@ -63,20 +75,22 @@ pub mod windows;
/// e.g. `44100/(16384/2) == 5.383Hz`, i.e. more samples => better accuracy
/// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]`
/// * `frequency_limit` Frequency limit. See [`FrequencyLimit´]
/// * `per_element_scaling_fn` Optional per element scaling function, e.g. `20 * log(x)`.
/// To see where this equation comes from, check out
/// this paper:
/// https://www.sjsu.edu/people/burford.furman/docs/me120/FFT_tutorial_NI.pdf
/// * `total_scaling_fn` See [`crate::spectrum::SpectrumTotalScaleFunctionFactory`].
/// * `per_element_scaling_fn` See [`crate::SimpleSpectrumScalingFunction`] for details.
/// This is easier to write, especially for Rust beginners. Everything
/// that can be achieved with this, can also be achieved with
/// parameter `total_scaling_fn`.
/// See [`crate::scaling`] for example implementations.
/// * `total_scaling_fn` See [`crate::spectrum::SpectrumTotalScaleFunctionFactory`] for details.
/// See [`crate::scaling`] for example implementations.
///
/// ## Returns value
/// New object of type [`FrequencySpectrum`].
pub fn samples_fft_to_spectrum(
samples: &[f32],
sampling_rate: u32,
frequency_limit: FrequencyLimit,
per_element_scaling_fn: Option<&dyn Fn(f32) -> f32>,
total_scaling_fn: Option<SpectrumTotalScaleFunctionFactory>,
per_element_scaling_fn: Option<SimpleSpectrumScalingFunction>,
total_scaling_fn: Option<ComplexSpectrumScalingFunction>,
) -> FrequencySpectrum {
// With FFT we transform an array of time-domain waveform samples
// into an array of frequency-domain spectrum samples
Expand Down Expand Up @@ -150,13 +164,19 @@ fn fft_result_to_frequency_to_magnitude_map(
sampling_rate: u32,
frequency_limit: FrequencyLimit,
per_element_scaling_fn: Option<&dyn Fn(f32) -> f32>,
total_scaling_fn: Option<SpectrumTotalScaleFunctionFactory>,
total_scaling_fn: Option<ComplexSpectrumScalingFunction>,
) -> FrequencySpectrum {
let maybe_min = frequency_limit.maybe_min();
let maybe_max = frequency_limit.maybe_max();

let samples_len = fft_result.len();

// see documentation of fft_calc_frequency_resolution for better explanation
let frequency_resolution = fft_calc_frequency_resolution(
sampling_rate,
samples_len as u32,
);

// collect frequency => frequency value in Vector of Pairs/Tuples
let frequency_vec = fft_result
.into_iter()
Expand All @@ -167,7 +187,9 @@ fn fft_result_to_frequency_to_magnitude_map(
// calc index => corresponding frequency
.map(|(fft_index, complex)| {
(
fft_index_to_corresponding_frequency(fft_index, samples_len as u32, sampling_rate),
// corresponding frequency of each index of FFT result
// see documentation of fft_calc_frequency_resolution for better explanation
fft_index as f32 * frequency_resolution,
complex,
)
})
Expand Down Expand Up @@ -202,41 +224,52 @@ fn fft_result_to_frequency_to_magnitude_map(
.collect::<Vec<(Frequency, FrequencyValue)>>();

// create spectrum object
let fs = FrequencySpectrum::new(frequency_vec);
let spectrum = FrequencySpectrum::new(
frequency_vec,
frequency_resolution,
);

// optionally scale
if let Some(total_scaling_fn) = total_scaling_fn {
fs.apply_total_scaling_fn(total_scaling_fn)
spectrum.apply_complex_scaling_fn(total_scaling_fn)
}
fs

spectrum
}

/// Calculate what index in the FFT result corresponds to what frequency.
/// Calculate the frequency resolution of the FFT. It is determined by the sampling rate
/// in Hertz and N, the number of samples given into the FFT. With the frequency resolution,
/// we can determine the corresponding frequency of each index in the FFT result buffer.
///
/// ## Parameters
/// * `fft_index` Index in FFT result buffer. If `samples.len() == 2048` then this is in `{0, 1, ..., 1023}`
/// * `samples_len` Number of samples put into the FFT
/// * `sampling_rate` sampling_rate, e.g. `44100 [Hz]`
///
/// ## Return value
/// 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)]
fn fft_index_to_corresponding_frequency(
fft_index: usize,
samples_len: u32,
fn fft_calc_frequency_resolution(
sampling_rate: u32,
samples_len: u32,
) -> f32 {
// Explanation for the algorithm:
// https://stackoverflow.com/questions/4364823/

// samples : [0], [1], [2], [3], ... , ..., [2047] => 2048 samples for example
// FFT Result : [0], [1], [2], [3], ... , ..., [2047]
// Relevant part of FFT Result: [0], [1], [2], [3], ... , [1023]
// Relevant part of FFT Result: [0], [1], [2], [3], ... , [1023] => first N/2 results important
// ^ ^
// Frequency : 0Hz, .................... Sampling Rate/2
// 0Hz is also called (e.g. 22050Hz @ 44100H sampling rate)
// 0Hz is also called (e.g. 22050Hz for 44100H sampling rate)
// "DC Component"

// frequency step/resolution is for example: 1/1024 * 44100
// 1024: relevant FFT result, 2048 samples, 44100 sample rate
// frequency step/resolution is for example: 1/2048 * 44100
// 2048 samples, 44100 sample rate

fft_index as f32 / samples_len as f32 * sampling_rate as f32
// equal to: 1.0 / samples_len as f32 * sampling_rate as f32
sampling_rate as f32 / samples_len as f32
}
67 changes: 67 additions & 0 deletions src/scaling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
MIT License
Copyright (c) 2021 Philipp Schuster
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
//! This module contains convenient public transform functions that you can use
//! as parameters in [`crate::samples_fft_to_spectrum`].

/// Practical implementations for [`crate::SimpleSpectrumScalingFunction`].
pub mod basic {
/// Calculates the base 10 logarithm of each frequency magnitude and
/// multiplies it with 20. This scaling is quite common, you can
/// find more information for example here:
/// https://www.sjsu.edu/people/burford.furman/docs/me120/FFT_tutorial_NI.pdf
///
/// ## Usage
/// ```rust
///use spectrum_analyzer::{samples_fft_to_spectrum, scaling, FrequencyLimit};
///let window = [0.0, 0.1, 0.2, 0.3]; // add real data here
///let spectrum = samples_fft_to_spectrum(
/// &window,
/// 44100,
/// FrequencyLimit::All,
/// Some(&scaling::basic::scale_20_times_log10),
/// None,
/// );
/// ```
pub fn scale_20_times_log10(frequency_magnitude: f32) -> f32 {
20.0 * frequency_magnitude.log10()
}
}

/// Practical implementations for [`crate::spectrum::ComplexSpectrumScalingFunction`].
pub mod complex {
use alloc::boxed::Box;
use crate::ComplexSpectrumScalingFunction;

/// Returns a function factory that generates a function that scales
/// each frequency value/amplitude in the spectrum to interval `[0.0; 1.0]`.
pub fn scale_to_zero_to_one() -> ComplexSpectrumScalingFunction {
Box::new(
move |_min: f32, max: f32, _average: f32, _median: f32| {
Box::new(
move |x| x / max
)
}
)
}
}
Loading

0 comments on commit c30344a

Please sign in to comment.