use crate::error::SpectrumAnalyzerError;
use crate::scaling::{divide_by_N, scale_to_zero_to_one};
use crate::tests::sine::sine_wave_audio_data_multiple;
use crate::windows::{blackman_harris_4term, blackman_harris_7term, hamming_window, hann_window};
use crate::{FrequencyLimit, samples_fft_to_spectrum};
use alloc::vec::Vec;
use audio_visualizer::Channels;
use audio_visualizer::spectrum::plotters_png_file::spectrum_static_plotters_png_visualize;
use audio_visualizer::waveform::plotters_png_file::waveform_static_plotters_png_visualize;
use core::cmp::max;
use std::path::PathBuf;
fn test_out_dir() -> PathBuf {
let path = std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let dir = PathBuf::from(dir);
dir.join("target")
});
let path = path.join("test_generated");
if !path.exists() {
let _ = std::fs::create_dir(path.clone());
}
path
}
mod sine;
#[test]
#[cfg_attr(miri, ignore)] fn test_spectrum_and_visualize_sine_waves_50_1000_3777hz() {
let sine_audio = sine_wave_audio_data_multiple(&[50.0, 1000.0, 3777.0], 44100, 1000);
waveform_static_plotters_png_visualize(
&sine_audio,
Channels::Mono,
test_out_dir().to_str().unwrap(),
"test_spectrum_and_visualize_sine_waves_50_1000_3777hz--WAVEFORM.png",
);
let sine_audio = sine_audio
.into_iter()
.map(|x| x as f32)
.collect::<Vec<f32>>();
let window = &sine_audio[0..4096];
let out_dir = test_out_dir();
let out_dir = out_dir.to_str().unwrap();
let spectra = [
("no-window", window.to_vec()),
("hamming-window", hamming_window(window)),
("hann-window", hann_window(window)),
(
"blackman-harris-4-term-window",
blackman_harris_4term(window),
),
(
"blackman-harris-7-term-window",
blackman_harris_7term(window),
),
]
.into_iter()
.map(|(filename_suffix, samples)| {
let spectrum = samples_fft_to_spectrum(
&samples,
44100,
FrequencyLimit::Max(4000.0),
Some(&scale_to_zero_to_one),
)
.unwrap();
spectrum_static_plotters_png_visualize(
&spectrum.to_map(),
out_dir,
&format!(
"test_spectrum_and_visualize_sine_waves_50_1000_3777hz--{filename_suffix}.png"
),
);
(filename_suffix, spectrum)
})
.collect::<Vec<_>>();
let spectrum_hann_window = &spectra
.iter()
.find(|(filename_suffix, _spectrum)| *filename_suffix == "hann-window")
.unwrap()
.1;
assert!(spectrum_hann_window.freq_val_exact(50.0).val() > 0.85);
assert!(spectrum_hann_window.freq_val_closest(50.0).1.val() > 0.85);
assert!(spectrum_hann_window.freq_val_exact(1000.0).val() > 0.85);
assert!(spectrum_hann_window.freq_val_closest(1000.0).1.val() > 0.85);
assert!(spectrum_hann_window.freq_val_exact(3777.0).val() > 0.85);
assert!(spectrum_hann_window.freq_val_closest(3777.0).1.val() > 0.85);
assert!(spectrum_hann_window.freq_val_exact(500.0).val() < 0.00001);
assert!(spectrum_hann_window.freq_val_closest(500.0).1.val() < 0.00001);
}
#[test]
#[cfg_attr(miri, ignore)] fn test_spectrum_power() {
let interesting_frequency = 2048.0;
let sine_audio = sine_wave_audio_data_multiple(&[interesting_frequency], 44100, 1000);
let sine_audio = sine_audio
.into_iter()
.map(|x| x as f32)
.collect::<Vec<f32>>();
let short_window = &sine_audio[0..2048];
let long_window = &sine_audio[0..4096];
let spectrum_short_window = samples_fft_to_spectrum(
short_window,
44100,
FrequencyLimit::Max(4000.0),
Some(÷_by_N),
)
.unwrap();
let spectrum_long_window = samples_fft_to_spectrum(
long_window,
44100,
FrequencyLimit::Max(4000.0),
Some(÷_by_N),
)
.unwrap();
spectrum_static_plotters_png_visualize(
&spectrum_short_window.to_map(),
test_out_dir().to_str().unwrap(),
"test_spectrum_power__short_window.png",
);
spectrum_static_plotters_png_visualize(
&spectrum_long_window.to_map(),
test_out_dir().to_str().unwrap(),
"test_spectrum_power__long_window.png",
);
let a = spectrum_short_window.freq_val_exact(interesting_frequency);
let b = spectrum_long_window.freq_val_exact(interesting_frequency);
let ab_abs_diff = (a - b).val().abs();
let ab_deviation = ab_abs_diff / max(a, b).val();
assert!(
ab_deviation < 0.122,
"Values must more or less equal, because both were divided by their N. deviation={ab_deviation}"
);
}
#[test]
fn test_spectrum_frequency_limit_inclusive() {
let sampling_rate = 1024;
let sine_audio = sine_wave_audio_data_multiple(&[512.0], sampling_rate, 1000);
let sine_audio = sine_audio
.into_iter()
.map(|x| x as f32)
.collect::<Vec<f32>>();
let window = hann_window(&sine_audio[0..512]);
{
let spectrum =
samples_fft_to_spectrum(&window, sampling_rate, FrequencyLimit::Max(400.0), None)
.unwrap();
assert_eq!(
spectrum.min_fr().val(),
0.0,
"Lower bound frequency must be inclusive!"
);
assert_eq!(
spectrum.max_fr().val(),
400.0,
"Upper bound frequency must be inclusive!"
);
}
{
let spectrum =
samples_fft_to_spectrum(&window, sampling_rate, FrequencyLimit::Min(100.0), None)
.unwrap();
assert_eq!(
spectrum.min_fr().val(),
100.0,
"Lower bound frequency must be inclusive!"
);
assert_eq!(
spectrum.max_fr().val(),
sampling_rate as f32 / 2.0,
"Upper bound frequency must be inclusive!"
);
}
{
let spectrum = samples_fft_to_spectrum(
&window,
sampling_rate,
FrequencyLimit::Range(412.0, 510.0),
None,
)
.unwrap();
assert_eq!(
spectrum.min_fr().val(),
412.0,
"Lower bound frequency must be inclusive!"
);
assert_eq!(
spectrum.max_fr().val(),
510.0,
"Upper bound frequency must be inclusive!"
);
}
}
#[test]
#[cfg_attr(miri, ignore)] fn test_spectrum_nyquist_theorem() {
let dummy_audio_samples = vec![0.0; 4096];
let spectrum =
samples_fft_to_spectrum(&dummy_audio_samples, 44100, FrequencyLimit::All, None).unwrap();
assert_eq!(
4096 / 2 + 1,
spectrum
.data()
.iter()
.map(|x| x.1)
.filter(|x| x.val() == 0.0)
.count(),
"All frequency values must be exactly zero because the input signal is zero!"
);
assert_eq!(
0.0,
spectrum.min_fr().val(),
"Minimum frequency must be 0 Hz (DS Component/DC bias/Gleichwert)"
);
assert_eq!(
44100.0 / 2.0,
spectrum.max_fr().val(),
"Maximum frequency must be Nyquist frequency"
);
}
#[test]
#[cfg_attr(miri, ignore)] fn test_spectrum_nyquist_theorem2() {
let sine_audio = sine_wave_audio_data_multiple(
&[22049.9],
44100,
1000,
)
.into_iter()
.map(|x| x as f32)
.collect::<Vec<f32>>();
let spectrum = samples_fft_to_spectrum(
&sine_audio[0..4096],
44100,
FrequencyLimit::All,
Some(&scale_to_zero_to_one),
)
.unwrap();
assert_eq!(
0.0,
spectrum.min_fr().val(),
"Maximum frequency must be Nyquist 0 Hz (DS Component/DC bias/Gleichwert)"
);
assert_eq!(
44100.0 / 2.0,
spectrum.max_fr().val(),
"Maximum frequency must be Nyquist frequency"
);
assert!(
spectrum.max().1.val() > 0.99,
"Nyquist frequency must have a notable peak"
);
assert!(
spectrum.freq_val_exact(22049.9).val() >= 0.94,
"Other frequencies must not be part of the spectrum!"
);
assert!(
spectrum.freq_val_exact(22049.0).val() >= 0.49,
"Other frequencies must not be part of the spectrum!"
);
assert!(
spectrum.freq_val_exact(22035.0).val() <= 0.26,
"Other frequencies must not be part of the spectrum!"
);
assert!(
spectrum.freq_val_exact(22000.0).val() <= 0.07,
"Other frequencies must not be part of the spectrum!"
);
assert!(
spectrum.freq_val_exact(21500.0).val() <= 0.01,
"Other frequencies must not be part of the spectrum!"
);
}
#[test]
fn test_invalid_input() {
let samples = vec![0.0, 1.0, 2.0, 3.0, f32::NAN, 4.0, 5.0, 6.0];
let err = samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::All, None).unwrap_err();
assert!(matches!(err, SpectrumAnalyzerError::NaNValuesNotSupported));
let samples = vec![0.0, 1.0, 2.0, 3.0, f32::INFINITY, 4.0, 5.0, 6.0];
let err = samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::All, None).unwrap_err();
assert!(matches!(
err,
SpectrumAnalyzerError::InfinityValuesNotSupported
));
let samples = vec![0.0];
let err = samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::All, None).unwrap_err();
assert!(matches!(err, SpectrumAnalyzerError::TooFewSamples));
let samples = vec![0.0; 4];
let err =
samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::Min(-1.0), None).unwrap_err();
assert!(matches!(
err,
SpectrumAnalyzerError::InvalidFrequencyLimit(_)
));
let samples = vec![0.0; 8];
let err =
samples_fft_to_spectrum(&samples, 8, FrequencyLimit::Range(1.1, 1.9), None).unwrap_err();
assert!(matches!(
err,
SpectrumAnalyzerError::FrequencyLimitTooNarrow
));
let err =
samples_fft_to_spectrum(&samples, 8, FrequencyLimit::Range(1.0, 1.0), None).unwrap_err();
assert!(matches!(
err,
SpectrumAnalyzerError::FrequencyLimitTooNarrow
));
let samples = vec![0.0; 3];
let err = samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::All, None).unwrap_err();
assert!(matches!(
err,
SpectrumAnalyzerError::SamplesLengthNotAPowerOfTwo
));
}
#[test]
fn test_only_null_samples_valid() {
let samples = vec![0.0, 0.0];
let _ = samples_fft_to_spectrum(&samples, 44100, FrequencyLimit::All, None).unwrap();
}
#[test]
fn test_scaling_produces_error() {
let samples = vec![1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];
let _ = samples_fft_to_spectrum(
&samples,
44100,
FrequencyLimit::All,
Some(&|_val, _info| f32::NAN),
)
.expect_err("Must throw error due to illegal scaling!");
}
#[test]
#[cfg_attr(miri, ignore)] fn test_divide_by_n_has_effect() {
let audio_data = sine_wave_audio_data_multiple(&[100.0, 200.0, 400.0], 1000, 2000);
let audio_data = audio_data.into_iter().map(|x| x as f32).collect::<Vec<_>>();
let audio_data = hann_window(&audio_data[0..1024]);
let normal_spectrum =
samples_fft_to_spectrum(&audio_data, 1000, FrequencyLimit::All, None).unwrap();
let scaled_spectrum =
samples_fft_to_spectrum(&audio_data, 1000, FrequencyLimit::All, Some(÷_by_N))
.unwrap();
for i in 0..512 {
let actual_no_scaling = normal_spectrum.data()[i].1.val();
let actual_with_scaling = scaled_spectrum.data()[i].1.val();
assert!(
(actual_no_scaling / 1024.0 - actual_with_scaling) < 0.1,
"[{i}] actual_no_scaling={actual_no_scaling} should be roughly 1024 times bigger than actual_with_scaling={actual_with_scaling}",
);
}
let scaled_spectrum_with_limit = samples_fft_to_spectrum(
&audio_data,
1000,
FrequencyLimit::Max(250.0),
Some(÷_by_N),
)
.unwrap();
for i in 0..256 {
let reference = scaled_spectrum.data()[i].1.val();
let actual = scaled_spectrum_with_limit.data()[i].1.val();
assert_eq!(
reference, actual,
"having less frequencies in the spectrum due to a limit must not effect N!"
);
}
}