spectrum_analyzer/scaling.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//! This module contains convenient public transform functions that you can use
25//! as parameters in [`samples_fft_to_spectrum`] for scaling the frequency value
26//! (the FFT result).
27//!
28//! They act as "idea/inspiration". Feel free to either compose them or create
29//! your own derivation from them.
30//!
31//! [`samples_fft_to_spectrum`]: crate::samples_fft_to_spectrum
32use alloc::boxed::Box;
33
34/// Helper struct for [`SpectrumScalingFunction`] that is passed into the
35/// scaling function together with the current frequency value.
36///
37/// This structure can be used to scale each value. All properties reference the
38/// current data of a [`FrequencySpectrum`].
39///
40/// This uses `f32` in favor of [`FrequencyValue`] because the latter led to
41/// some implementation problems.
42///
43/// [`FrequencySpectrum`]: crate::FrequencySpectrum
44/// [`FrequencyValue`]: crate::FrequencyValue
45#[derive(Debug)]
46pub struct SpectrumDataStats {
47 /// Minimal frequency value in spectrum.
48 pub min: f32,
49 /// Maximum frequency value in spectrum.
50 pub max: f32,
51 /// Average frequency value in spectrum.
52 pub average: f32,
53 /// Median frequency value in spectrum.
54 pub median: f32,
55 /// Number of samples (`samples.len()`). Already casted to f32, to avoid
56 /// repeatedly casting in a loop for each value.
57 pub n: f32,
58}
59
60/// Describes the type for a function that scales/normalizes the data inside
61/// [`FrequencySpectrum`].
62///
63/// The scaling only affects the value/amplitude of the frequency, but not the
64/// frequency itself. It is applied to every single element.
65///
66/// A scaling function can be used for example to subtract the minimum (`min`)
67/// from each value. It is optional to use the second parameter
68/// [`SpectrumDataStats`].
69///
70/// The type works with static functions as well as dynamically created
71/// closures.
72///
73/// You must take care of, that you don't have division by zero in your function
74/// or that the result is NaN or Infinity (regarding IEEE-754). If the result
75/// is NaN or Infinity, the library will return `Err`.
76///
77/// This uses `f32` in favor of [`FrequencyValue`] because the latter led to
78/// some implementation problems.
79///
80/// [`FrequencySpectrum`]: crate::FrequencySpectrum
81/// [`FrequencyValue`]: crate::FrequencyValue
82pub type SpectrumScalingFunction = dyn Fn(f32, &SpectrumDataStats) -> f32;
83
84/// Calculates the base 10 logarithm of each frequency magnitude and
85/// multiplies it with 20.
86///
87/// This scaling is quite common, you can find more information for example
88/// here:
89/// <https://www.sjsu.edu/people/burford.furman/docs/me120/FFT_tutorial_NI.pdf>
90///
91/// ## Usage
92/// ```rust
93///use spectrum_analyzer::{samples_fft_to_spectrum, scaling, FrequencyLimit};
94///let window = [0.0, 0.1, 0.2, 0.3]; // add real data here
95///let spectrum = samples_fft_to_spectrum(
96/// &window,
97/// 44100,
98/// FrequencyLimit::All,
99/// Some(&scaling::scale_20_times_log10),
100/// );
101/// ```
102/// Function is of type [`SpectrumScalingFunction`].
103#[must_use]
104pub fn scale_20_times_log10(fr_val: f32, _stats: &SpectrumDataStats) -> f32 {
105 debug_assert!(!fr_val.is_infinite());
106 debug_assert!(!fr_val.is_nan());
107 debug_assert!(fr_val >= 0.0);
108 if fr_val == 0.0 {
109 0.0
110 } else {
111 20.0 * libm::log10f(fr_val)
112 }
113}
114
115/// Scales each frequency value/amplitude in the spectrum to interval `[0.0; 1.0]`.
116/// Function is of type [`SpectrumScalingFunction`]. Expects that [`SpectrumDataStats::min`] is
117/// not negative.
118#[must_use]
119pub fn scale_to_zero_to_one(fr_val: f32, stats: &SpectrumDataStats) -> f32 {
120 debug_assert!(!fr_val.is_infinite());
121 debug_assert!(!fr_val.is_nan());
122 debug_assert!(fr_val >= 0.0);
123 if stats.max != 0.0 {
124 fr_val / stats.max
125 } else {
126 0.0
127 }
128}
129
130/// Divides each value by N. Several resources recommend that the FFT result should be divided
131/// by the length of samples, so that values of different samples lengths are comparable.
132#[allow(non_snake_case)]
133#[must_use]
134pub fn divide_by_N(fr_val: f32, stats: &SpectrumDataStats) -> f32 {
135 debug_assert!(!fr_val.is_infinite());
136 debug_assert!(!fr_val.is_nan());
137 debug_assert!(fr_val >= 0.0);
138 if stats.n == 0.0 {
139 fr_val
140 } else {
141 fr_val / stats.n
142 }
143}
144
145/// Like [`divide_by_N`] but divides each value by `sqrt(N)`.
146///
147/// This is the recommended scaling in the `rustfft` documentation (but is
148/// generally applicable).
149/// See <https://docs.rs/rustfft/latest/rustfft/#normalization>
150#[allow(non_snake_case)]
151#[must_use]
152pub fn divide_by_N_sqrt(fr_val: f32, stats: &SpectrumDataStats) -> f32 {
153 debug_assert!(!fr_val.is_infinite());
154 debug_assert!(!fr_val.is_nan());
155 debug_assert!(fr_val >= 0.0);
156 if stats.n == 0.0 {
157 fr_val
158 } else {
159 // https://docs.rs/rustfft/latest/rustfft/#normalization
160 fr_val / libm::sqrtf(stats.n)
161 }
162}
163
164/// Combines several scaling functions into a new single one.
165///
166/// Currently there is the limitation that the functions need to have
167/// a `'static` lifetime. This will be fixed if someone needs this.
168///
169/// # Example
170/// ```
171/// use spectrum_analyzer::scaling::{combined, divide_by_N, scale_20_times_log10};
172/// let fncs = combined(&[÷_by_N, &scale_20_times_log10]);
173/// ```
174pub fn combined(fncs: &'static [&SpectrumScalingFunction]) -> Box<SpectrumScalingFunction> {
175 Box::new(move |val, stats| {
176 let mut val = val;
177 for fnc in fncs {
178 val = fnc(val, stats);
179 }
180 val
181 })
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use alloc::vec::Vec;
188
189 #[test]
190 fn test_scale_to_zero_to_one() {
191 let data = vec![0.0_f32, 1.1, 2.2, 3.3, 4.4, 5.5];
192 let stats = SpectrumDataStats {
193 min: data[0],
194 max: data[data.len() - 1],
195 average: data.iter().sum::<f32>() / data.len() as f32,
196 median: (2.2 + 3.3) / 2.0,
197 n: data.len() as f32,
198 };
199 // check that type matches
200 let scaling_fn: &SpectrumScalingFunction = &scale_to_zero_to_one;
201 let scaled_data = data
202 .into_iter()
203 .map(|x| scaling_fn(x, &stats))
204 .collect::<Vec<_>>();
205 let expected = [0.0_f32, 0.2, 0.4, 0.6, 0.8, 1.0];
206 for (expected_val, actual_val) in expected.iter().zip(scaled_data.iter()) {
207 float_cmp::approx_eq!(f32, *expected_val, *actual_val, ulps = 3);
208 }
209 }
210
211 // make sure this compiles
212 #[test]
213 fn test_combined_compiles() {
214 let _combined_static = combined(&[&scale_20_times_log10, ÷_by_N, ÷_by_N_sqrt]);
215
216 // doesn't compile yet.. fix this once someone requests it
217 /*let closure_scaling_fnc = |fr_val: f32, _stats: &SpectrumDataStats| {
218 0.0
219 };
220
221 let _combined_dynamic = combined(&[
222 &scale_20_times_log10,
223 ÷_by_N,
224 ÷_by_N_sqrt,
225 &closure_scaling_fnc,
226 ]);*/
227 }
228}