~signal_processor/tfft

46785be6ff8f86a44058572be99f30a5966e9b0e — signal_Processor 10 months ago 3efea8b
Add a wrapper around audio_backend that inits an fft for our purposes. Make fft processing generic over num::Floats
3 files changed, 98 insertions(+), 137 deletions(-)

D fft/Cargo.lock
M fft/Cargo.toml
M fft/src/lib.rs
D fft/Cargo.lock => fft/Cargo.lock +0 -99
@@ 1,99 0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "fft"
version = "0.1.0"
dependencies = [
 "realfft",
]

[[package]]
name = "num-complex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
dependencies = [
 "num-traits",
]

[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
 "autocfg",
 "num-traits",
]

[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
 "autocfg",
]

[[package]]
name = "primal-check"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0"
dependencies = [
 "num-integer",
]

[[package]]
name = "realfft"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571"
dependencies = [
 "rustfft",
]

[[package]]
name = "rustfft"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434"
dependencies = [
 "num-complex",
 "num-integer",
 "num-traits",
 "primal-check",
 "strength_reduce",
 "transpose",
 "version_check",
]

[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"

[[package]]
name = "transpose"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23"
dependencies = [
 "num-integer",
 "strength_reduce",
]

[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

M fft/Cargo.toml => fft/Cargo.toml +4 -0
@@ 7,3 7,7 @@ edition = "2021"

[dependencies]
realfft = "3.3.0"
audio_backend = {git="https://git.sr.ht/~signal_processor/audio_backend"}
num = "0.4.1"

blockfree-chan = {git = "https://git.sr.ht/~signal_processor/blockfree_chan"}

M fft/src/lib.rs => fft/src/lib.rs +94 -38
@@ 1,3 1,11 @@
use std::sync::{RwLock, Arc};

use audio_backend::node_interface::{Metadata, Processor};
use blockfree_chan::{BlockFreeTx, blockfree_channel};
use num::{Float, Signed, FromPrimitive};

pub use audio_backend::Backend;
pub use blockfree_chan::BlockFreeRx;
///Helper functions and their data for running and configuring several overlapping ffts

use realfft::num_complex::Complex;


@@ 12,15 20,15 @@ pub enum Windows {
}


pub fn make_window(len: usize, window: Windows) -> Vec<f32> {
pub fn make_window<T: Float>(len: usize, window: Windows) -> Vec<T> {
    (0..len).map(|i| {
        match window {
            Windows::Rect => 1.0,
            Windows::Hann => 0.5*(1.0-(2.0*std::f32::consts::PI*i as f32/len as f32).cos()),
            Windows::Rect => T::one(),
            Windows::Hann => T::from(0.5*(1.0-(2.0*std::f32::consts::PI*i as f32/len as f32).cos())).unwrap(),
            Windows::Hamming => {
                let a0 = 25.0/46.0;
                let a1 = 1.0-a0;
                a0-(a1*2.0*std::f32::consts::PI*i as f32/len as f32).cos()
                T::from(a0-(a1*2.0*std::f32::consts::PI*i as f32/len as f32).cos()).unwrap()
            },
            Windows::BlackmanNutall => {
                let x = 2.0*std::f32::consts::PI*i as f32/len as f32;


@@ 28,7 36,7 @@ pub fn make_window(len: usize, window: Windows) -> Vec<f32> {
                let a1 = 0.4891775;
                let a2 = 0.1365995;
                let a3 = 0.0106411;
                a0-a1*(x).cos()+a2*(2.0*x.cos())-a3*(3.0*x.cos())
                T::from(a0-a1*(x).cos()+a2*(2.0*x.cos())-a3*(3.0*x.cos())).unwrap()
            },
        }
    }).collect()


@@ 42,69 50,117 @@ pub struct FftSpec {
}

///Data needed for maintaining an fft stream over some audio data.
pub struct FftState {
    fft: std::sync::Arc<dyn realfft::RealToComplex<f32>>,
    in_buf: Vec<f32>,
pub struct FftState<T: Float> {
    fft: std::sync::Arc<dyn realfft::RealToComplex<T>>,
    in_buf: Vec<T>,
    //separate from the in_buf so we can have overlaps. Since the fft is stateless we can just run
    //the same fft for every window, using different slices from the running_buf
    running_buf: Vec<f32>,
    running_buf: Vec<T>,
    //Window function, as a table.
    window: Vec<f32>,
    pub spectrum: Vec<Complex<f32>>,
    scratch_buf: Vec<Complex<f32>>,
    window: Vec<T>,
    pub spectrum: Vec<Complex<T>>,
    scratch_buf: Vec<Complex<T>>,
    idx: u32,
    pub spec: FftSpec,
    //determines if the fft data has been read before.
    pub stale: bool,
    bins_tx: BlockFreeTx<Vec<T>>,
    vec: Vec<T>,
}

impl FftState {
impl<T: Float+std::fmt::Debug+Send+Sync+Signed+FromPrimitive+'static> FftState<T> {
    //Initialize the state for an fft given a spec and a window type.
    pub fn new(spec: FftSpec, window: Windows) -> Self {
        let fft = realfft::RealFftPlanner::<f32>::new()
                                                 .plan_fft_forward(spec.num_bins as usize);
    pub fn new(spec: FftSpec, window: Windows, bins_tx: blockfree_chan::BlockFreeTx<Vec<T>>) -> Self {
        let fft = realfft::RealFftPlanner::<T>::new()
                                               .plan_fft_forward(spec.num_bins as usize);
        let spectrum = fft.make_output_vec();
        let in_buf = fft.make_input_vec();
        let scratch_buf = fft.make_scratch_vec();
        let running_buf = in_buf.clone();
        let vec = vec![T::zero(); spec.num_bins as usize/2];
        Self {
            fft,
            idx: 0,
            spectrum,
            in_buf,
            bins_tx,
            running_buf,
            scratch_buf,
            window: make_window(spec.num_bins as usize, window),
            spec,
            vec,
            stale: true,
        }
    }
}

impl FftState {
    pub fn reset(&mut self) {
impl<T: Float+Send+Sync> Processor for FftState<T> {
    type SampleType = T;

    fn process(&mut self, audio: &mut audio_backend::node_interface::Audio<Self::SampleType>) {
        for frame in audio.input_frames() {
            for samp in frame.take(1) {
                let buf_len = self.running_buf.len() as u32;
                self.running_buf[self.idx as usize] = *samp; 

                let new_idx = (self.idx+1)%buf_len;
                if new_idx%(buf_len/self.spec.overlap) == 0 {
                    self.in_buf.iter_mut()
                               .zip(self.running_buf.iter().cycle().skip(self.idx as usize))
                               .zip(self.window.iter())
                               .for_each(|((old, new), window_cf)| {
                        let windowed = *new**window_cf;
                        *old = windowed;
                    });
                    let _ = self.fft.process_with_scratch(&mut self.in_buf, &mut self.spectrum, &mut self.scratch_buf);
                    // self.stale = false;
                //normalize the levels of each bin.
                let l = self.vec.len();
                self.spectrum.iter().zip(self.vec.iter_mut()).for_each(|(x, y)| {
                    let x1 = x.norm_sqr();
                    *y = x1/T::from(l).unwrap();
                });
                //send the bins to the ui thread. We use this method so that we can avoid allocating a
                //new vec, and thus breaking our realtime requirements.
                self.bins_tx.write_by(&self.vec, &mut |old: &mut Vec<T>, new: &Vec<T>| {
                    old.copy_from_slice(new);
                });
                }
                self.idx = new_idx;
            }
        }
    }

    fn reset(&mut self) {
        self.idx = 0;
        self.running_buf.fill(0.0);
        self.running_buf.fill(T::zero());
    }

    ///Intake a sample. If enough samples have been received, calculate a spectrum, accumulate it
    ///to the existing spectrum, and send the existing spectrum via the blockfree channel.
    pub fn process_sample(&mut self, samp: f32) {
        let buf_len = self.running_buf.len() as u32;
        self.running_buf[self.idx as usize] = samp; 

        let new_idx = (self.idx+1)%buf_len;
        if new_idx%(buf_len/self.spec.overlap) == 0 {
            self.in_buf.iter_mut()
                       .zip(self.running_buf.iter().cycle().skip(self.idx as usize))
                       .zip(self.window.iter())
                       .for_each(|((old, new), window_cf)| {
                let windowed = *new*window_cf;
                *old = windowed;
            });
            let _ = self.fft.process_with_scratch(&mut self.in_buf, &mut self.spectrum, &mut self.scratch_buf);
            self.stale = false;
        }
        self.idx = new_idx;
    fn update(&mut self) {
        
    }

    fn params(&mut self) -> &mut dyn audio_backend::node_interface::Param {
        todo!()    
    }

    fn prepare(&mut self, spec: &audio_backend::node_interface::ProcessSpec) {
       self.reset(); 
    }

    fn metadata(&self) -> Metadata {
        Metadata{inputs: vec!["Input".into()], outputs: vec![], name: "FFT".into()}
    }
}

pub fn run_fft(name: &str, backend: audio_backend::Backend, window: Windows, spec: FftSpec) -> BlockFreeRx<Vec<f32>> {
    let (bins_tx, bins_rx) = blockfree_channel(&vec![0.0; spec.num_bins as usize/2]);

    let fft_node: FftState<f32> = FftState::new(spec, window, bins_tx);
    let mut meta = fft_node.metadata();
    meta.name = name.into();

    audio_backend::run(backend, &meta, Arc::new(RwLock::new(fft_node)));
    bins_rx
}