ruststft/
lib.rs

1/*!
2
3**computes the [short-time fourier transform](https://en.wikipedia.org/wiki/Short-time_Fourier_transform)
4on streaming data.**
5
6to use add `ruststft = "*"`
7to the `[dependencies]` section of your `Cargo.toml` and `use ruststft;` in your code.
8
9## example
10
11```
12use ruststft::{STFT, WindowType};
13
14// let's generate ten seconds of fake audio
15let sample_rate: usize = 44100;
16let seconds: usize = 10;
17let sample_count = sample_rate * seconds;
18let all_samples = (0..sample_count).map(|x| x as f64).collect::<Vec<f64>>();
19
20// let's initialize our short-time fourier transform
21let window_type: WindowType = WindowType::Hanning;
22let window_size: usize = 1024;
23let step_size: usize = 512;
24let mut stft = STFT::new(window_type, window_size, step_size);
25
26// we need a buffer to hold a computed column of the spectrogram
27let mut spectrogram_column: Vec<f64> =
28    std::iter::repeat(0.).take(stft.output_size()).collect();
29
30// iterate over all the samples in chunks of 3000 samples.
31// in a real program you would probably read from something instead.
32for some_samples in (&all_samples[..]).chunks(3000) {
33    // append the samples to the internal ringbuffer of the stft
34    stft.append_samples(some_samples);
35
36    // as long as there remain window_size samples in the internal
37    // ringbuffer of the stft
38    while stft.contains_enough_to_compute() {
39        // compute one column of the stft by
40        // taking the first window_size samples of the internal ringbuffer,
41        // multiplying them with the window,
42        // computing the fast fourier transform,
43        // taking half of the symetric complex outputs,
44        // computing the norm of the complex outputs and
45        // taking the log10
46        stft.compute_column(&mut spectrogram_column[..]);
47
48        // here's where you would do something with the
49        // spectrogram_column...
50
51        // drop step_size samples from the internal ringbuffer of the stft
52        // making a step of size step_size
53        stft.move_to_next_column();
54    }
55}
56```
57*/
58
59use num::complex::Complex;
60use num::traits::{Float, Signed, Zero};
61use rustfft::{Fft, FftDirection, FftNum, FftPlanner};
62use std::str::FromStr;
63use std::sync::Arc;
64use strider::{SliceRing, SliceRingImpl};
65
66/// returns `0` if `log10(value).is_negative()`.
67/// otherwise returns `log10(value)`.
68/// `log10` turns values in domain `0..1` into values
69/// in range `-inf..0`.
70/// `log10_positive` turns values in domain `0..1` into `0`.
71/// this sets very small values to zero which may not be
72/// what you want depending on your application.
73#[inline]
74pub fn log10_positive<T: Float + Signed + Zero>(value: T) -> T {
75    // Float.log10
76    // Signed.is_negative
77    // Zero.zero
78    let log = value.log10();
79    if log.is_negative() {
80        T::zero()
81    } else {
82        log
83    }
84}
85
86/// the type of apodization window to use
87#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
88pub enum WindowType {
89    Hanning,
90    Hamming,
91    Blackman,
92    Nuttall,
93    None,
94}
95
96impl FromStr for WindowType {
97    type Err = &'static str;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        let lower = s.to_lowercase();
101        match lower.as_str() {
102            "hanning" => Ok(WindowType::Hanning),
103            "hann" => Ok(WindowType::Hanning),
104            "hamming" => Ok(WindowType::Hamming),
105            "blackman" => Ok(WindowType::Blackman),
106            "nuttall" => Ok(WindowType::Nuttall),
107            "none" => Ok(WindowType::None),
108            _ => Err("no match"),
109        }
110    }
111}
112
113// this also implements ToString::to_string
114impl std::fmt::Display for WindowType {
115    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
116        write!(formatter, "{:?}", self)
117    }
118}
119
120// TODO write a macro that does this automatically for any enum
121static WINDOW_TYPES: [WindowType; 5] = [
122    WindowType::Hanning,
123    WindowType::Hamming,
124    WindowType::Blackman,
125    WindowType::Nuttall,
126    WindowType::None,
127];
128
129impl WindowType {
130    pub fn values() -> [WindowType; 5] {
131        WINDOW_TYPES
132    }
133}
134
135pub struct STFT<T>
136where
137    T: FftNum + FromF64 + num::Float,
138{
139    pub window_size: usize,
140    pub fft_size: usize,
141    pub step_size: usize,
142    pub fft: Arc<dyn Fft<T>>,
143    pub window: Option<Vec<T>>,
144    /// internal ringbuffer used to store samples
145    pub sample_ring: SliceRingImpl<T>,
146    pub real_input: Vec<T>,
147    pub complex_input_output: Vec<Complex<T>>,
148    fft_scratch: Vec<Complex<T>>,
149}
150
151impl<T> STFT<T>
152where
153    T: FftNum + FromF64 + num::Float,
154{
155    pub fn window_type_to_window_vec(
156        window_type: WindowType,
157        window_size: usize,
158    ) -> Option<Vec<T>> {
159        match window_type {
160            WindowType::Hanning => Some(
161                apodize::hanning_iter(window_size)
162                    .map(FromF64::from_f64)
163                    .collect(),
164            ),
165            WindowType::Hamming => Some(
166                apodize::hamming_iter(window_size)
167                    .map(FromF64::from_f64)
168                    .collect(),
169            ),
170            WindowType::Blackman => Some(
171                apodize::blackman_iter(window_size)
172                    .map(FromF64::from_f64)
173                    .collect(),
174            ),
175            WindowType::Nuttall => Some(
176                apodize::nuttall_iter(window_size)
177                    .map(FromF64::from_f64)
178                    .collect(),
179            ),
180            WindowType::None => None,
181        }
182    }
183
184    pub fn new(window_type: WindowType, window_size: usize, step_size: usize) -> Self {
185        let window = Self::window_type_to_window_vec(window_type, window_size);
186        Self::new_with_window_vec(window, window_size, step_size)
187    }
188
189    pub fn new_with_zero_padding(
190        window_type: WindowType,
191        window_size: usize,
192        fft_size: usize,
193        step_size: usize,
194    ) -> Self {
195        let window = Self::window_type_to_window_vec(window_type, window_size);
196        Self::new_with_window_vec_and_zero_padding(window, window_size, fft_size, step_size)
197    }
198
199    // TODO this should ideally take an iterator and not a vec
200    pub fn new_with_window_vec_and_zero_padding(
201        window: Option<Vec<T>>,
202        window_size: usize,
203        fft_size: usize,
204        step_size: usize,
205    ) -> Self {
206        assert!(step_size > 0 && step_size < window_size);
207        let fft = FftPlanner::new().plan_fft(fft_size, FftDirection::Forward);
208
209        // allocate a scratch buffer for the FFT
210        let scratch_len = fft.get_inplace_scratch_len();
211        let fft_scratch = vec![Complex::<T>::zero(); scratch_len];
212
213        STFT {
214            window_size,
215            fft_size,
216            step_size,
217            fft,
218            fft_scratch,
219            sample_ring: SliceRingImpl::new(),
220            window,
221            real_input: std::iter::repeat(T::zero()).take(window_size).collect(),
222            // zero-padded complex_input, so the size is fft_size, not window_size
223            complex_input_output: std::iter::repeat(Complex::<T>::zero())
224                .take(fft_size)
225                .collect(),
226            // same size as complex_output
227        }
228    }
229
230    pub fn new_with_window_vec(
231        window: Option<Vec<T>>,
232        window_size: usize,
233        step_size: usize,
234    ) -> Self {
235        Self::new_with_window_vec_and_zero_padding(window, window_size, window_size, step_size)
236    }
237
238    #[inline]
239    pub fn output_size(&self) -> usize {
240        self.fft_size / 2
241    }
242
243    #[inline]
244    pub fn len(&self) -> usize {
245        self.sample_ring.len()
246    }
247
248    #[inline]
249    pub fn is_empty(&self) -> bool {
250        self.len() == 0
251    }
252
253    pub fn append_samples(&mut self, input: &[T]) {
254        self.sample_ring.push_many_back(input);
255    }
256
257    #[inline]
258    pub fn contains_enough_to_compute(&self) -> bool {
259        self.window_size <= self.sample_ring.len()
260    }
261
262    pub fn compute_into_complex_output(&mut self) {
263        assert!(self.contains_enough_to_compute());
264
265        // read into real_input
266        self.sample_ring.read_many_front(&mut self.real_input);
267
268        // multiply real_input with window
269        if let Some(ref window) = self.window {
270            for (dst, src) in self.real_input.iter_mut().zip(window.iter()) {
271                *dst = *dst * *src;
272            }
273        }
274
275        // copy windowed real_input as real parts into complex_input
276        // only copy `window_size` size, leave the rest in `complex_input` be zero
277        for (src, dst) in self
278            .real_input
279            .iter()
280            .zip(self.complex_input_output.iter_mut())
281        {
282            dst.re = *src;
283            dst.im = T::zero();
284        }
285
286        // ensure the buffer is indeed zero-padded when needed.
287        if self.window_size < self.fft_size {
288            for dst in self.complex_input_output.iter_mut().skip(self.window_size) {
289                dst.re = T::zero();
290                dst.im = T::zero();
291            }
292        }
293
294        // compute fft
295        self.fft
296            .process_with_scratch(&mut self.complex_input_output, &mut self.fft_scratch)
297    }
298
299    /// # Panics
300    /// panics unless `self.output_size() == output.len()`
301    pub fn compute_complex_column(&mut self, output: &mut [Complex<T>]) {
302        assert_eq!(self.output_size(), output.len());
303
304        self.compute_into_complex_output();
305
306        for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) {
307            *dst = *src;
308        }
309    }
310
311    /// # Panics
312    /// panics unless `self.output_size() == output.len()`
313    pub fn compute_magnitude_column(&mut self, output: &mut [T]) {
314        assert_eq!(self.output_size(), output.len());
315
316        self.compute_into_complex_output();
317
318        for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) {
319            *dst = src.norm();
320        }
321    }
322
323    /// computes a column of the spectrogram
324    /// # Panics
325    /// panics unless `self.output_size() == output.len()`
326    pub fn compute_column(&mut self, output: &mut [T]) {
327        assert_eq!(self.output_size(), output.len());
328
329        self.compute_into_complex_output();
330
331        for (dst, src) in output.iter_mut().zip(self.complex_input_output.iter()) {
332            *dst = log10_positive(src.norm());
333        }
334    }
335
336    /// make a step
337    /// drops `self.step_size` samples from the internal buffer `self.sample_ring`.
338    pub fn move_to_next_column(&mut self) {
339        self.sample_ring.drop_many_front(self.step_size);
340    }
341
342    /// corresponding frequencies of a column of the spectrogram
343    /// # Arguments
344    /// `fs`: sampling frequency.
345    pub fn freqs(&self, fs: f64) -> Vec<f64> {
346        let n_freqs = self.output_size();
347        (0..n_freqs)
348            .map(|f| (f as f64) / ((n_freqs - 1) as f64) * (fs / 2.))
349            .collect()
350    }
351
352    /// corresponding time of first columns of the spectrogram
353    pub fn first_time(&self, fs: f64) -> f64 {
354        (self.window_size as f64) / (fs * 2.)
355    }
356
357    /// time interval between two adjacent columns of the spectrogram
358    pub fn time_interval(&self, fs: f64) -> f64 {
359        (self.step_size as f64) / fs
360    }
361}
362
363pub trait FromF64 {
364    fn from_f64(n: f64) -> Self;
365}
366
367impl FromF64 for f64 {
368    fn from_f64(n: f64) -> Self {
369        n
370    }
371}
372
373impl FromF64 for f32 {
374    fn from_f64(n: f64) -> Self {
375        n as f32
376    }
377}