Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc updates and preparations for v1.6.0 #61

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ jobs:

- run: cargo run --release --example mp3-samples

miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
components: miri
- uses: Swatinem/rust-cache@v2
- name: Install required Linux packages for "audio-visualizer"/cpal/minifb
run: sudo apt update && sudo apt -y install libasound2-dev libxkbcommon-dev
- run: cargo miri test --all-features

style_checks:
runs-on: ubuntu-latest
strategy:
Expand Down
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Changelog

## Unreleased
- Added FFT buffer size of 32768
## Unreleased (yet)


## 1.6.0 (2024-12-16)
## 1.6.0 (2024-12-17)
- dependency updates
- MSRV bump but only for the tests and examples, not library users
- Added FFT buffer size of 32768
- Optimized implementation, resulting in less unnecessary copying of data
- Removed excessive stack usage for large input data

## 1.5.0 (2023-09-21)
- fixed the build by updating the dependencies
Expand Down
99 changes: 0 additions & 99 deletions examples/bench.rs

This file was deleted.

27 changes: 21 additions & 6 deletions examples/mp3-samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,24 @@ use spectrum_analyzer::scaling::scale_to_zero_to_one;
use spectrum_analyzer::windows::{blackman_harris_4term, hamming_window, hann_window};
use spectrum_analyzer::{samples_fft_to_spectrum, FrequencyLimit};
use std::fs::File;
use std::path::PathBuf;
use std::time::Instant;

const TEST_OUT_DIR: &str = "test/out";
/// Returns the location where tests should store files they produce.
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() {
std::fs::create_dir(path.clone()).unwrap();
}
path
}

fn main() {
println!("bass drum example:");
Expand Down Expand Up @@ -213,31 +228,31 @@ fn to_spectrum_and_plot(

spectrum_static_plotters_png_visualize(
&spectrum_no_window.to_map(),
TEST_OUT_DIR,
test_out_dir().to_str().unwrap(),
&format!("{}--no-window.png", filename),
);

spectrum_static_plotters_png_visualize(
&spectrum_hamming_window.to_map(),
TEST_OUT_DIR,
test_out_dir().to_str().unwrap(),
&format!("{}--hamming-window.png", filename),
);

spectrum_static_plotters_png_visualize(
&spectrum_hann_window.to_map(),
TEST_OUT_DIR,
test_out_dir().to_str().unwrap(),
&format!("{}--hann-window.png", filename),
);

spectrum_static_plotters_png_visualize(
&spectrum_blackman_harris_4term_window.to_map(),
TEST_OUT_DIR,
test_out_dir().to_str().unwrap(),
&format!("{}--blackman-harris-4-term-window.png", filename),
);

spectrum_static_plotters_png_visualize(
&spectrum_blackman_harris_7term_window.to_map(),
TEST_OUT_DIR,
test_out_dir().to_str().unwrap(),
&format!("{}--blackman-harris-7-term-window.png", filename),
);
}
Expand Down
109 changes: 83 additions & 26 deletions src/fft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,120 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

//! Real FFT using [`microfft::real`] that is very fast and also works in `no_std`
//! environments. It is faster than regular fft (with the `rustfft` crate for
//! example). The difference to a complex FFT, as with `rustfft` is, that the
//! result vector contains less results as there are no mirrored frequencies.
//! Real FFT using [`microfft::real`] that is very fast and also works in
//! `no_std` environments. It is faster than regular fft (with the `rustfft`
//! crate for example). The difference to a complex FFT, as with `rustfft` is,
//! that the result vector contains fewer results as there are no mirrored
//! frequencies.

/// FFT base result type.
pub use microfft::Complex32;

use alloc::vec::Vec;
use core::convert::TryInto;
use core::mem;
use microfft::real;

/// The result of a FFT is always complex but because different FFT crates might
/// use different versions of "num-complex", each implementation exports
/// it's own version that gets used in lib.rs for binary compatibility.
pub use microfft::Complex32;

/// Calculates the real FFT by invoking the proper function corresponding to the
/// buffer length.
/// Calculates the FFT by invoking the function of [`microfft::real`] that
/// corresponds to the input size.
macro_rules! real_fft_n {
($buffer:expr, $( $i:literal ),*) => {
match $buffer.len() {
$(
$i => {
let mut buffer: [_; $i] = $buffer.try_into().unwrap();
let fixed_size_view = $buffer.as_mut_slice().try_into().unwrap();
paste::paste! (
real::[<rfft_$i>]
)(&mut buffer).to_vec()
)(fixed_size_view)
}
)*
_ => { unimplemented!("unexpected buffer len") }
_ => { unimplemented!("should be one of the supported buffer lengths, but was {}", $buffer.len()) }
}
};
}

/// Real FFT using [`microfft::real`].
/// FFT using [`microfft::real`].
pub struct FftImpl;

impl FftImpl {
/// Calculates the FFT For the given input samples and returns a Vector of
/// of [`Complex32`] with length `samples.len() / 2 + 1`, where the first
/// index corresponds to the DC component and the last index to the Nyquist
/// frequency.
/// Calculates the FFT For the given input samples and returns a [`Vec`] of
/// [`Complex32`] with length `samples.len() / 2 + 1`.
///
/// The first index corresponds to the DC component and the last index to
/// the Nyquist frequency.
///
/// # Parameters
/// - `samples`: Array with samples. Each value must be a regular floating
/// point number (no NaN or infinite) and the length must be
/// a power of two. Otherwise, the function panics.
#[inline]
pub(crate) fn calc(samples: &[f32]) -> Vec<Complex32> {
let mut fft_res: Vec<Complex32> = real_fft_n!(
samples, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768
assert_eq!(
samples.len() % 2,
0,
"buffer length must be a multiple of two!"
);
let mut vec_buffer = Vec::with_capacity(samples.len() + 2 /* Nyquist */);
assert_eq!(
vec_buffer.capacity() % 2,
0,
"vector capacity must be a multiple of two for safe casting!"
);

vec_buffer.extend_from_slice(samples);

// The result is a view into the buffer.
// We discard the view and directly operate on the buffer.
let _fft_res: &mut [Complex32] = real_fft_n!(
&mut vec_buffer,
2,
4,
8,
16,
32,
64,
128,
256,
512,
1024,
2048,
4096,
8192,
16384,
32768
);

// We transform the original vector while preserving its memory, to
// prevent any reallocation or unnecessary copying.
let mut buffer = {
let ptr = vec_buffer.as_mut_ptr().cast::<Complex32>();
let len = vec_buffer.len() / 2;
let capacity = vec_buffer.capacity() / 2;
let new_buffer_view = unsafe { Vec::from_raw_parts(ptr, len, capacity) };
mem::forget(vec_buffer);
new_buffer_view
};

// `microfft::real` documentation says: the Nyquist frequency real value
// is packed inside the imaginary part of the DC component.
let nyquist_fr_pos_val = fft_res[0].im;
fft_res[0].im = 0.0;
// manually add the nyquist frequency
fft_res.push(Complex32::new(nyquist_fr_pos_val, 0.0));
fft_res
let nyquist_fr_pos_val = buffer[0].im;
buffer[0].im = 0.0;
// manually add the Nyquist frequency
buffer.push(Complex32::new(nyquist_fr_pos_val, 0.0));
buffer
}
}

#[cfg(test)]
mod tests {
use crate::fft::FftImpl;

/// This test is primarily for miri.
#[test]
fn test_memory_safety() {
let samples = [1.0, 2.0, 3.0, 4.0];
let fft = FftImpl::calc(&samples);

assert_eq!(fft.len(), 2 + 1);
}
}
Loading
Loading