~nickbp/soundview

ab173ef0d496418f6e1e5f8faeb7a9839f16656c — Nick Parker 4 months ago 82bbe37
Implement windowed FFT (untested)

Seems to run but haven't checked for valid output
5 files changed, 146 insertions(+), 9 deletions(-)

M Cargo.lock
M Cargo.toml
M src/fourier.rs
M src/main.rs
M src/recorder.rs
M Cargo.lock => Cargo.lock +65 -0
@@ 30,6 30,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "cache-padded"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 139,6 145,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"

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

[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 170,6 185,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"

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

[[package]]
name = "proc-macro2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 212,6 236,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"

[[package]]
name = "ringbuf"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c60f3923939c33e6c543ddbff14d0ee6a407fcd186d560be37282559616adf3"
dependencies = [
 "cache-padded",
]

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

[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 279,12 326,20 @@ dependencies = [
 "anyhow",
 "crossbeam-channel",
 "git-testament",
 "ringbuf",
 "rustfft",
 "sdl2",
 "tracing",
 "tracing-subscriber",
]

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

[[package]]
name = "syn"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 397,6 452,16 @@ dependencies = [
]

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

[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +3 -1
@@ 12,6 12,8 @@ repository = "https://sr.ht/~nickbp/soundview/"
anyhow = "1.0"
crossbeam-channel = "0.5"
git-testament = "0.2"
ringbuf = "0.2"
rustfft = "6.0"
sdl2 = "0.35"
tracing = "0.1"
tracing-subscriber = "0.2"
\ No newline at end of file
tracing-subscriber = "0.2"

M src/fourier.rs => src/fourier.rs +72 -5
@@ 1,13 1,80 @@
use crossbeam_channel::{Receiver, Sender};
use ringbuf::RingBuffer;
use rustfft::{FftPlanner, num_complex::Complex};
use tracing::{debug, warn};

pub fn process_audio_loop(recv_audio: Receiver<Vec<f32>>, send_processed: Sender<Vec<f32>>) {
use std::cmp;
use std::f32::consts::PI;

// copied from window.rs in dsp 0.10.1 - dual licensed MIT/Apache2
// https://docs.rs/dsp/0.10.1/src/dsp/window.rs.html#151-159
fn hann(width: usize, offset: usize, window_length: usize) -> Vec<f32> {
    let mut samples = vec![0.0; window_length];
    let end = cmp::min(offset + width, window_length);
    for i in offset..end {
        let n = (i - offset) as f32;
        samples[i] = (PI * n / (width - 1) as f32).sin().powi(2);
    }
    samples
}

pub fn process_audio_loop(fft_size: usize, recv_audio: Receiver<Vec<f32>>, send_processed: Sender<Vec<f32>>) {
    let fft = FftPlanner::new().plan_fft_forward(fft_size);
    // Have the edges of the FFT input be zeroes
    let window = hann(fft_size - 2, 1, fft_size);

    // Buffer for accumulating incoming audio, before it is processed by the FFT
    let (mut audio_buf_in, mut audio_buf_out) = RingBuffer::new(fft_size).split();
    let mut fft_buf = Vec::with_capacity(fft_size);
    fft_buf.resize(fft_size, Complex::new(0.0, 0.0));

    loop {
        match recv_audio.recv() {
            Ok(mut audio) => {
                audio.truncate(5); // TODO actual processing...
                if let Err(e) = send_processed.send(audio) {
                    warn!("Failed to send processed data: {}", e);
            Ok(audio) => {
                if audio_buf_in.capacity() < audio_buf_in.len() + audio.len() {
                    // Grow circular buffer capacity to match what's needed
                    let (mut new_audio_buf_in, new_audio_buf_out) = RingBuffer::new(audio_buf_in.capacity() + audio.len()).split();
                    new_audio_buf_in.move_from(&mut audio_buf_out, None);
                    audio_buf_in = new_audio_buf_in;
                    audio_buf_out = new_audio_buf_out;
                }
                audio_buf_in.push_iter(&mut audio.into_iter());

                while audio_buf_out.len() >= fft_size {
                    // There's enough buffered input data to run a round of FFT
                    // Copy fft_size values from audio_buf to fft_buf, applying hann window along the way
                    // There's a little extra complexity here because the ringbuf exposes the content as two parts
                    audio_buf_out.access(|older, newer| {
                        if older.len() >= fft_size {
                            // just read from older
                            for i in 0..fft_size {
                                fft_buf[i] = Complex::new(window[i] * older[i], 0.0);
                            }
                        } else {
                            // read from older, then newer
                            for i in 0..older.len() {
                                fft_buf[i] = Complex::new(window[i] * older[i], 0.0);
                            }
                            for i in 0..(fft_size - older.len()) {
                                fft_buf[i] = Complex::new(window[i] * newer[i], 0.0);
                            }
                        }
                    });

                    // Remove the first HALF of the values from audio_buf, shifting it forward.
                    // The second half is left in-place.
                    // This allows us to reuse parts of the data that were deemphasized by the window filter.
                    // To illustrate:
                    // 1: 0 1 2 3 2 1 0
                    // 2:       0 1 2 3 2 1 0
                    // 3:             0 1 2 3 2 1 0
                    audio_buf_out.discard(fft_size / 2);

                    // Process the selected data in fft_buf, then forward the result
                    fft.process(&mut fft_buf);
                    if let Err(e) = send_processed.send(Vec::from_iter(fft_buf.iter().map(|c| c.norm()))) {
                        warn!("Failed to send processed data: {}", e);
                    }
                }
            },
            Err(e) => {

M src/main.rs => src/main.rs +2 -1
@@ 61,7 61,8 @@ fn main() -> Result<()> {
        thread::Builder::new()
            .name("fourier".to_string())
            .spawn(move || {
                fourier::process_audio_loop(recv_audio, send_processed)
                // TODO expose fourier size as a cmdline option?
                fourier::process_audio_loop(1024, recv_audio, send_processed)
            })?
    );


M src/recorder.rs => src/recorder.rs +4 -2
@@ 1,5 1,5 @@
use anyhow::{anyhow, Result};
use tracing::{info, warn};
use tracing::{error, info};

use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
use sdl2::AudioSubsystem;


@@ 13,7 13,9 @@ impl AudioCallback for Callback {

    fn callback(&mut self, out: &mut [f32]) {
        if let Err(e) = self.audio_out.send(Vec::from(out)) {
            warn!("Failed to send audio: {}", e);
            // This implies that the fourier thread has panicked
            // (A full queue would just result in a block)
            error!("Failed to send audio: {}", e);
        }
    }
}