diff --git a/.gitignore b/.gitignore index 7883767..f1d8a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target **.DS_Store Cargo.lock +.idea/ diff --git a/Cargo.toml b/Cargo.toml index 3bc28e6..bca2040 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,18 @@ name = "stft" readme = "README.md" repository = "https://github.com/snd/stft.git" version = "0.2.0" +edition = "2018" [dependencies] -apodize = "0.3.1" -num = "0.2.0" -rustfft = "3.0.0" +apodize = "1.0.0" +num = "0.4.0" +rustfft = "6.0.1" strider = "0.1.3" + +[dev-dependencies] +criterion = "0.3.4" +approx = "0.5.0" + +[[bench]] +name = "lib" +harness = false diff --git a/benches/lib.rs b/benches/lib.rs index f893b8e..6b6d752 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -1,98 +1,134 @@ -#![feature(test)] -extern crate test; - -extern crate num; +use criterion::{criterion_group, criterion_main, Criterion}; use num::complex::Complex; - -extern crate rustfft; -use rustfft::FFT; - -extern crate stft; -use stft::{STFT, WindowType}; +use rustfft::{FftDirection, FftPlanner}; +use stft::{WindowType, STFT}; macro_rules! bench_fft_process { - ($bencher:expr, $window_size:expr, $float:ty) => {{ - let inverse = false; - let window_size = $window_size; - let mut fft = FFT::<$float>::new(window_size, inverse); - let input = std::iter::repeat(Complex::new(0., 0.)) - .take(window_size) - .collect::>>(); + ($c:expr, $window_size:expr, $float:ty) => {{ + let mut planner = FftPlanner::new(); + let fft = planner.plan_fft($window_size, FftDirection::Forward); + // input is processed in-place let mut output = std::iter::repeat(Complex::new(0., 0.)) - .take(window_size) + .take($window_size) .collect::>>(); - $bencher.iter(|| { - fft.process(&input[..], &mut output[..]) - }); - - }} + $c.bench_function( + concat!( + "bench_fft_process_", + stringify!($window_size), + "_", + stringify!($float) + ), + |b| b.iter(|| fft.process(&mut output[..])), + ); + }}; } -#[bench] -fn bench_fft_process_1024_f32(bencher: &mut test::Bencher) { - bench_fft_process!(bencher, 1024, f32); +fn bench_fft_process_1024_f32(c: &mut Criterion) { + bench_fft_process!(c, 1024, f32); } -#[bench] -fn bench_fft_process_1024_f64(bencher: &mut test::Bencher) { - bench_fft_process!(bencher, 1024, f64); +fn bench_fft_process_1024_f64(c: &mut Criterion) { + bench_fft_process!(c, 1024, f64); } +criterion_group!( + benches_fft_process, + bench_fft_process_1024_f32, + bench_fft_process_1024_f64 +); + macro_rules! bench_stft_compute { - ($bencher:expr, $window_size:expr, $float:ty) => {{ - let mut stft = STFT::<$float>::new(WindowType::Hanning, $window_size, 0); - let input = std::iter::repeat(1.).take($window_size).collect::>(); - let mut output = std::iter::repeat(0.).take(stft.output_size()).collect::>(); + ($c:expr, $window_size:expr, $float:ty) => {{ + let step_size: usize = 512; + let mut stft = STFT::<$float>::new(WindowType::Hanning, $window_size, step_size); + let input = std::iter::repeat(1.) + .take($window_size) + .collect::>(); + let mut output = std::iter::repeat(0.) + .take(stft.output_size()) + .collect::>(); stft.append_samples(&input[..]); - $bencher.iter(|| { - stft.compute_column(&mut output[..]) - }); - }} + $c.bench_function( + concat!( + "bench_stft_compute_", + stringify!($window_size), + "_", + stringify!($float) + ), + |b| b.iter(|| stft.compute_column(&mut output[..])), + ); + }}; } -#[bench] -fn bench_stft_compute_1024_f32(bencher: &mut test::Bencher) { - bench_stft_compute!(bencher, 1024, f32); +fn bench_stft_compute_1024_f32(c: &mut Criterion) { + bench_stft_compute!(c, 1024, f32); } -#[bench] -fn bench_stft_compute_1024_f64(bencher: &mut test::Bencher) { - bench_stft_compute!(bencher, 1024, f64); +fn bench_stft_compute_1024_f64(c: &mut Criterion) { + bench_stft_compute!(c, 1024, f64); } +criterion_group!( + benches_stft_compute, + bench_stft_compute_1024_f32, + bench_stft_compute_1024_f64 +); + macro_rules! bench_stft_audio { - ($bencher:expr, $seconds:expr, $float:ty) => {{ + ($c:expr, $seconds:expr, $float:ty) => {{ // let's generate some fake audio let sample_rate: usize = 44100; let seconds: usize = $seconds; let sample_count = sample_rate * seconds; - let all_samples = (0..sample_count).map(|x| x as $float).collect::>(); - $bencher.iter(|| { - // let's initialize our short-time fourier transform - let window_type: WindowType = WindowType::Hanning; - let window_size: usize = 1024; - let step_size: usize = 512; - let mut stft = STFT::<$float>::new(window_type, window_size, step_size); - // we need a buffer to hold a computed column of the spectrogram - let mut spectrogram_column: Vec<$float> = - std::iter::repeat(0.).take(stft.output_size()).collect(); - for some_samples in (&all_samples[..]).chunks(3000) { - stft.append_samples(some_samples); - while stft.contains_enough_to_compute() { - stft.compute_column(&mut spectrogram_column[..]); - stft.move_to_next_column(); - } - } - }); - }} + let all_samples = (0..sample_count) + .map(|x| x as $float) + .collect::>(); + $c.bench_function( + concat!( + "bench_stft_audio_", + stringify!($windowsize), + "_", + stringify!($float) + ), + |b| { + b.iter(|| { + // let's initialize our short-time fourier transform + let window_type: WindowType = WindowType::Hanning; + let window_size: usize = 1024; + let step_size: usize = 512; + let mut stft = STFT::<$float>::new(window_type, window_size, step_size); + // we need a buffer to hold a computed column of the spectrogram + let mut spectrogram_column: Vec<$float> = + std::iter::repeat(0.).take(stft.output_size()).collect(); + for some_samples in (&all_samples[..]).chunks(3000) { + stft.append_samples(some_samples); + while stft.contains_enough_to_compute() { + stft.compute_column(&mut spectrogram_column[..]); + stft.move_to_next_column(); + } + } + }) + }, + ); + }}; } -#[bench] -fn bench_stft_10_seconds_audio_f32(bencher: &mut test::Bencher) { - bench_stft_audio!(bencher, 10, f32); +fn bench_stft_10_seconds_audio_f32(c: &mut Criterion) { + bench_stft_audio!(c, 10, f32); } -#[bench] -fn bench_stft_10_seconds_audio_f64(bencher: &mut test::Bencher) { - bench_stft_audio!(bencher, 10, f64); +fn bench_stft_10_seconds_audio_f64(c: &mut Criterion) { + bench_stft_audio!(c, 10, f64); } + +criterion_group!( + benches_stft_audio, + bench_stft_10_seconds_audio_f32, + bench_stft_10_seconds_audio_f64 +); + +criterion_main!( + benches_fft_process, + benches_stft_compute, + benches_stft_audio +); diff --git a/src/lib.rs b/src/lib.rs index 39c1c2b..f0065cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,71 +9,60 @@ to the `[dependencies]` section of your `Cargo.toml` and call `extern crate stft ## example ``` -extern crate stft; use stft::{STFT, WindowType}; -fn main() { - // let's generate ten seconds of fake audio - let sample_rate: usize = 44100; - let seconds: usize = 10; - let sample_count = sample_rate * seconds; - let all_samples = (0..sample_count).map(|x| x as f64).collect::>(); - - // let's initialize our short-time fourier transform - let window_type: WindowType = WindowType::Hanning; - let window_size: usize = 1024; - let step_size: usize = 512; - let mut stft = STFT::new(window_type, window_size, step_size); - - // we need a buffer to hold a computed column of the spectrogram - let mut spectrogram_column: Vec = - std::iter::repeat(0.).take(stft.output_size()).collect(); - - // iterate over all the samples in chunks of 3000 samples. - // in a real program you would probably read from something instead. - for some_samples in (&all_samples[..]).chunks(3000) { - // append the samples to the internal ringbuffer of the stft - stft.append_samples(some_samples); - - // as long as there remain window_size samples in the internal - // ringbuffer of the stft - while stft.contains_enough_to_compute() { - // compute one column of the stft by - // taking the first window_size samples of the internal ringbuffer, - // multiplying them with the window, - // computing the fast fourier transform, - // taking half of the symetric complex outputs, - // computing the norm of the complex outputs and - // taking the log10 - stft.compute_column(&mut spectrogram_column[..]); - - // here's where you would do something with the - // spectrogram_column... - - // drop step_size samples from the internal ringbuffer of the stft - // making a step of size step_size - stft.move_to_next_column(); - } +// let's generate ten seconds of fake audio +let sample_rate: usize = 44100; +let seconds: usize = 10; +let sample_count = sample_rate * seconds; +let all_samples = (0..sample_count).map(|x| x as f64).collect::>(); + +// let's initialize our short-time fourier transform +let window_type: WindowType = WindowType::Hanning; +let window_size: usize = 1024; +let step_size: usize = 512; +let mut stft = STFT::new(window_type, window_size, step_size); + +// we need a buffer to hold a computed column of the spectrogram +let mut spectrogram_column: Vec = + std::iter::repeat(0.).take(stft.output_size()).collect(); + +// iterate over all the samples in chunks of 3000 samples. +// in a real program you would probably read from something instead. +for some_samples in (&all_samples[..]).chunks(3000) { + // append the samples to the internal ringbuffer of the stft + stft.append_samples(some_samples); + + // as long as there remain window_size samples in the internal + // ringbuffer of the stft + while stft.contains_enough_to_compute() { + // compute one column of the stft by + // taking the first window_size samples of the internal ringbuffer, + // multiplying them with the window, + // computing the fast fourier transform, + // taking half of the symetric complex outputs, + // computing the norm of the complex outputs and + // taking the log10 + stft.compute_column(&mut spectrogram_column[..]); + + // here's where you would do something with the + // spectrogram_column... + + // drop step_size samples from the internal ringbuffer of the stft + // making a step of size step_size + stft.move_to_next_column(); } } ``` */ -use std::str::FromStr; -use std::sync::Arc; - -extern crate num; use num::complex::Complex; use num::traits::{Float, Signed, Zero}; - -extern crate apodize; - -extern crate strider; +use rustfft::{Fft, FftDirection, FftNum, FftPlanner}; +use std::str::FromStr; +use std::sync::Arc; use strider::{SliceRing, SliceRingImpl}; -extern crate rustfft; -use rustfft::{FFT,FFTnum,FFTplanner}; - /// returns `0` if `log10(value).is_negative()`. /// otherwise returns `log10(value)`. /// `log10` turns values in domain `0..1` into values @@ -109,7 +98,7 @@ impl FromStr for WindowType { fn from_str(s: &str) -> Result { let lower = s.to_lowercase(); - match &lower[..] { + match lower.as_str() { "hanning" => Ok(WindowType::Hanning), "hann" => Ok(WindowType::Hanning), "hamming" => Ok(WindowType::Hamming), @@ -129,11 +118,13 @@ impl std::fmt::Display for WindowType { } // TODO write a macro that does this automatically for any enum -static WINDOW_TYPES: [WindowType; 5] = [WindowType::Hanning, - WindowType::Hamming, - WindowType::Blackman, - WindowType::Nuttall, - WindowType::None]; +static WINDOW_TYPES: [WindowType; 5] = [ + WindowType::Hanning, + WindowType::Hamming, + WindowType::Blackman, + WindowType::Nuttall, + WindowType::None, +]; impl WindowType { pub fn values() -> [WindowType; 5] { @@ -142,30 +133,50 @@ impl WindowType { } pub struct STFT - where T: FFTnum + FromF64 + num::Float +where + T: FftNum + FromF64 + num::Float, { pub window_size: usize, + pub fft_size: usize, pub step_size: usize, - pub fft: Arc>, + pub fft: Arc>, pub window: Option>, /// internal ringbuffer used to store samples pub sample_ring: SliceRingImpl, pub real_input: Vec, - pub complex_input: Vec>, - pub complex_output: Vec>, + pub complex_input_output: Vec>, + fft_scratch: Vec>, } impl STFT - where T: FFTnum + FromF64 + num::Float +where + T: FftNum + FromF64 + num::Float, { - pub fn window_type_to_window_vec(window_type: WindowType, - window_size: usize) - -> Option> { + pub fn window_type_to_window_vec( + window_type: WindowType, + window_size: usize, + ) -> Option> { match window_type { - WindowType::Hanning => Some(apodize::hanning_iter(window_size).map(FromF64::from_f64).collect()), - WindowType::Hamming => Some(apodize::hamming_iter(window_size).map(FromF64::from_f64).collect()), - WindowType::Blackman => Some(apodize::blackman_iter(window_size).map(FromF64::from_f64).collect()), - WindowType::Nuttall => Some(apodize::nuttall_iter(window_size).map(FromF64::from_f64).collect()), + WindowType::Hanning => Some( + apodize::hanning_iter(window_size) + .map(FromF64::from_f64) + .collect(), + ), + WindowType::Hamming => Some( + apodize::hamming_iter(window_size) + .map(FromF64::from_f64) + .collect(), + ), + WindowType::Blackman => Some( + apodize::blackman_iter(window_size) + .map(FromF64::from_f64) + .collect(), + ), + WindowType::Nuttall => Some( + apodize::nuttall_iter(window_size) + .map(FromF64::from_f64) + .collect(), + ), WindowType::None => None, } } @@ -175,38 +186,58 @@ impl STFT Self::new_with_window_vec(window, window_size, step_size) } + pub fn new_with_zero_padding( + window_type: WindowType, + window_size: usize, + fft_size: usize, + step_size: usize, + ) -> Self { + let window = Self::window_type_to_window_vec(window_type, window_size); + Self::new_with_window_vec_and_zero_padding(window, window_size, fft_size, step_size) + } + // TODO this should ideally take an iterator and not a vec - pub fn new_with_window_vec(window: Option>, - window_size: usize, - step_size: usize) - -> Self { - // TODO more assertions: - // window_size is power of two - // step_size > 0 - assert!(step_size <= window_size); - let inverse = false; - let mut planner = FFTplanner::new(inverse); + pub fn new_with_window_vec_and_zero_padding( + window: Option>, + window_size: usize, + fft_size: usize, + step_size: usize, + ) -> Self { + assert!(step_size > 0 && step_size < window_size); + let fft = FftPlanner::new().plan_fft(fft_size, FftDirection::Forward); + + // allocate a scratch buffer for the FFT + let scratch_len = fft.get_inplace_scratch_len(); + let fft_scratch = vec![Complex::::zero(); scratch_len]; + STFT { - window_size: window_size, - step_size: step_size, - fft: planner.plan_fft(window_size), + window_size, + fft_size, + step_size, + fft, + fft_scratch, sample_ring: SliceRingImpl::new(), - window: window, - real_input: std::iter::repeat(T::zero()) - .take(window_size) - .collect(), - complex_input: std::iter::repeat(Complex::::zero()) - .take(window_size) - .collect(), - complex_output: std::iter::repeat(Complex::::zero()) - .take(window_size) - .collect(), + window, + real_input: std::iter::repeat(T::zero()).take(window_size).collect(), + // zero-padded complex_input, so the size is fft_size, not window_size + complex_input_output: std::iter::repeat(Complex::::zero()) + .take(fft_size) + .collect(), + // same size as complex_output } } + pub fn new_with_window_vec( + window: Option>, + window_size: usize, + step_size: usize, + ) -> Self { + Self::new_with_window_vec_and_zero_padding(window, window_size, window_size, step_size) + } + #[inline] pub fn output_size(&self) -> usize { - self.window_size / 2 + self.fft_size / 2 } #[inline] @@ -214,6 +245,11 @@ impl STFT self.sample_ring.len() } + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn append_samples(&mut self, input: &[T]) { self.sample_ring.push_many_back(input); } @@ -227,7 +263,7 @@ impl STFT assert!(self.contains_enough_to_compute()); // read into real_input - self.sample_ring.read_many_front(&mut self.real_input[..]); + self.sample_ring.read_many_front(&mut self.real_input); // multiply real_input with window if let Some(ref window) = self.window { @@ -237,12 +273,27 @@ impl STFT } // copy windowed real_input as real parts into complex_input - for (dst, src) in self.complex_input.iter_mut().zip(self.real_input.iter()) { - dst.re = src.clone(); + // only copy `window_size` size, leave the rest in `complex_input` be zero + for (src, dst) in self + .real_input + .iter() + .zip(self.complex_input_output.iter_mut()) + { + dst.re = *src; + dst.im = T::zero(); + } + + // ensure the buffer is indeed zero-padded when needed. + if self.window_size < self.fft_size { + for dst in self.complex_input_output.iter_mut().skip(self.window_size) { + dst.re = T::zero(); + dst.im = T::zero(); + } } // compute fft - self.fft.process(&mut self.complex_input, &mut self.complex_output); + self.fft + .process_with_scratch(&mut self.complex_input_output, &mut self.fft_scratch) } /// # Panics @@ -252,8 +303,8 @@ impl STFT self.compute_into_complex_output(); - for (dst, src) in output.iter_mut().zip(self.complex_output.iter()) { - *dst = src.clone(); + for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) { + *dst = *src; } } @@ -264,7 +315,7 @@ impl STFT self.compute_into_complex_output(); - for (dst, src) in output.iter_mut().zip(self.complex_output.iter()) { + for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) { *dst = src.norm(); } } @@ -277,7 +328,7 @@ impl STFT self.compute_into_complex_output(); - for (dst, src) in output.iter_mut().zip(self.complex_output.iter()) { + for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) { *dst = log10_positive(src.norm()); } } @@ -287,6 +338,26 @@ impl STFT pub fn move_to_next_column(&mut self) { self.sample_ring.drop_many_front(self.step_size); } + + /// corresponding frequencies of a column of the spectrogram + /// # Arguments + /// `fs`: sampling frequency. + pub fn freqs(&self, fs: f64) -> Vec { + let n_freqs = self.output_size(); + (0..n_freqs) + .map(|f| (f as f64) / ((n_freqs - 1) as f64) * (fs / 2.)) + .collect() + } + + /// corresponding time of first columns of the spectrogram + pub fn first_time(&self, fs: f64) -> f64 { + (self.window_size as f64) / (fs * 2.) + } + + /// time interval between two adjacent columns of the spectrogram + pub fn time_interval(&self, fs: f64) -> f64 { + (self.step_size as f64) / fs + } } pub trait FromF64 { @@ -294,9 +365,13 @@ pub trait FromF64 { } impl FromF64 for f64 { - fn from_f64(n: f64) -> Self { n } + fn from_f64(n: f64) -> Self { + n + } } impl FromF64 for f32 { - fn from_f64(n: f64) -> Self { n as f32 } + fn from_f64(n: f64) -> Self { + n as f32 + } } diff --git a/tests/lib.rs b/tests/lib.rs index 3fdc11b..ff0a3ec 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,14 +1,22 @@ +use approx::assert_ulps_eq; use std::str::FromStr; - -extern crate stft; -use stft::{STFT, WindowType}; +use stft::{WindowType, STFT}; #[test] fn test_window_type_from_string() { - assert_eq!(WindowType::from_str("Hanning").unwrap(), WindowType::Hanning); - assert_eq!(WindowType::from_str("hanning").unwrap(), WindowType::Hanning); + assert_eq!( + WindowType::from_str("Hanning").unwrap(), + WindowType::Hanning + ); + assert_eq!( + WindowType::from_str("hanning").unwrap(), + WindowType::Hanning + ); assert_eq!(WindowType::from_str("hann").unwrap(), WindowType::Hanning); - assert_eq!(WindowType::from_str("blackman").unwrap(), WindowType::Blackman); + assert_eq!( + WindowType::from_str("blackman").unwrap(), + WindowType::Blackman + ); } #[test] @@ -20,7 +28,11 @@ fn test_window_type_to_string() { fn test_window_types_to_strings() { assert_eq!( vec!["Hanning", "Hamming", "Blackman", "Nuttall", "None"], - WindowType::values().iter().map(|x| x.to_string()).collect::>()); + WindowType::values() + .iter() + .map(|x| x.to_string()) + .collect::>() + ); } #[test] @@ -39,17 +51,76 @@ fn test_stft() { assert!(!stft.contains_enough_to_compute()); assert_eq!(stft.output_size(), 4); assert_eq!(stft.len(), 0); - stft.append_samples(&vec![500., 0., 100.][..]); + stft.append_samples(&[500., 0., 100.]); assert_eq!(stft.len(), 3); assert!(!stft.contains_enough_to_compute()); - stft.append_samples(&vec![500., 0., 100., 0.][..]); + stft.append_samples(&[500., 0., 100., 0.]); assert_eq!(stft.len(), 7); assert!(!stft.contains_enough_to_compute()); - stft.append_samples(&vec![500.][..]); + stft.append_samples(&[500.]); assert!(stft.contains_enough_to_compute()); let mut output: Vec = vec![0.; 4]; - stft.compute_column(&mut output[..]); + stft.compute_column(&mut output); println!("{:?}", output); + + let expected = vec![ + 2.7763337740785166, + 2.7149781042402594, + 2.6218024907053796, + 2.647816050270838, + ]; + assert_ulps_eq!(output.as_slice(), expected.as_slice(), max_ulps = 10); + + // repeat the calculation to ensure results are independent of the internal buffer + let mut output2: Vec = vec![0.; 4]; + stft.compute_column(&mut output2); + assert_ulps_eq!(output.as_slice(), output2.as_slice(), max_ulps = 10); +} + +#[test] +fn test_stft_padded() { + let mut stft = STFT::new_with_zero_padding(WindowType::Hanning, 8, 32, 4); + assert!(!stft.contains_enough_to_compute()); + assert_eq!(stft.output_size(), 16); + assert_eq!(stft.len(), 0); + stft.append_samples(&[500., 0., 100.]); + assert_eq!(stft.len(), 3); + assert!(!stft.contains_enough_to_compute()); + stft.append_samples(&[500., 0., 100., 0.]); + assert_eq!(stft.len(), 7); + assert!(!stft.contains_enough_to_compute()); + + stft.append_samples(&[500.]); + assert!(stft.contains_enough_to_compute()); + + let mut output: Vec = vec![0.; 16]; + stft.compute_column(&mut output); + println!("{:?}", output); + + let expected = vec![ + 2.7763337740785166, + 2.772158781619449, + 2.7598791705720664, + 2.740299218211912, + 2.7149781042402594, + 2.686495897766628, + 2.6585877421915676, + 2.635728083951981, + 2.6218024907053796, + 2.6183544930578027, + 2.6238833073831658, + 2.634925941918913, + 2.647816050270838, + 2.65977332745612, + 2.6691025866822033, + 2.6749381613735683, + ]; + assert_ulps_eq!(output.as_slice(), expected.as_slice(), max_ulps = 10); + + // repeat the calculation to ensure results are independent of the internal buffer + let mut output2: Vec = vec![0.; 16]; + stft.compute_column(&mut output2); + assert_ulps_eq!(output.as_slice(), output2.as_slice(), max_ulps = 10); }