From ff018a98e1e38f7735ee316788c8b5c0baf5132f Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Tue, 16 Feb 2021 22:44:31 +0100 Subject: [PATCH] Initial commit Start experimenting with Rust. This is a little experimental, but fully functional real-time FFT program based on the FFTW library. --- .gitignore | 1 + Cargo.lock | 298 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 12 ++ src/config.rs | 3 + src/main.rs | 67 +++++++++ src/signal_processing.rs | 114 +++++++++++++++ 6 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/signal_processing.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bbe9a81 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,298 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bstr" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" +dependencies = [ + "memchr", +] + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "fftw" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a61025ae24e60113038885a9ad24ef67b3f4df0b4300c98678c63e8ce3297f8" +dependencies = [ + "bitflags", + "failure", + "fftw-sys", + "lazy_static", + "ndarray", + "num-complex", + "num-traits", +] + +[[package]] +name = "fftw-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9489c013030134b2e49cdda9bee90ff9c3c23ae5330650cb84e10b6927b9ec66" +dependencies = [ + "libc", + "num-complex", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "itertools" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" + +[[package]] +name = "matrixmultiply" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcad67dcec2d58ff56f6292582377e6921afdf3bfbd533e26fb8900ae575e002" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mlua" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd179c80340f87911c17eb365ab9f890cf745ac722860d1465be5889be2853a" +dependencies = [ + "bstr", + "cc", + "lazy_static", + "num-traits", + "pkg-config", +] + +[[package]] +name = "musiclight" +version = "0.1.0" +dependencies = [ + "byteorder", + "fftw", + "mlua", +] + +[[package]] +name = "ndarray" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf380a8af901ad627594013a3bbac903ae0a6f94e176e47e46b5bbc1877b928" +dependencies = [ + "itertools", + "matrixmultiply", + "num-complex", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebac11a9d2e11f2af219b8b8d833b76b1ea0e054aa0e8d8e9e4cbde353bdf019" + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2de235c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "musiclight" +version = "0.1.0" +authors = ["Thomas Kolb "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +byteorder = "1.4" +fftw = { version = "0.6", default-features = false, features = ["system"] } +mlua = { version = "0.5", features = ["lua53"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3370f98 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,3 @@ +// definitions for the FFT +pub const BLOCK_LEN: usize = 512; +pub const SAMP_RATE: f32 = 48000.0; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2f18e15 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,67 @@ +// vim: noet + +use std::process::exit; + +use byteorder::{NativeEndian, ReadBytesExt}; + +use mlua::Lua; + +mod signal_processing; +mod config; + +use crate::signal_processing::SignalProcessing; + +fn main() +{ + let mut stdin = std::io::stdin(); + + // test the mlua crate + let lua_state = Lua::new(); + + lua_state.globals().set("get_rust_value", lua_state.create_function(|_, ()| { + Ok(3) + }).unwrap()).unwrap(); + + let user_script = std::fs::read_to_string("test.lua").unwrap(); + lua_state.load(&user_script).exec().unwrap(); + + let lua_func_test : mlua::Function = lua_state.globals().get("test").unwrap(); + + println!("{}", lua_func_test.call::<_, u32>(123).unwrap()); + + let mut sigproc = SignalProcessing::new(config::BLOCK_LEN, config::SAMP_RATE).unwrap(); + + println!("Done! Starting main loop…"); + + // array for samples directly read from stream + let mut samples = [0i16; config::BLOCK_LEN]; + + // main loop + loop { + + // read a block of samples and exit gracefully on EOF + for sample in samples.iter_mut() { + let res = stdin.read_i16::(); + + match res { + Ok(s) => *sample = s, + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + println!("End of stream. Exiting."); + exit(0); + }, + Err(e) => panic!(e) + } + } + + sigproc.import_i16_mono(&samples).unwrap(); + sigproc.update_fft().unwrap(); + + let energy_bass = sigproc.get_energy_in_band( 0.0, 400.0); + let energy_mid = sigproc.get_energy_in_band( 400.0, 4000.0); + let energy_treble = sigproc.get_energy_in_band(4000.0, config::SAMP_RATE/2.0); + + // dump the output + println!("Bass: {:8.2} – Mid: {:8.2} – Treble: {:8.2}", energy_bass, energy_mid, energy_treble); + } + +} diff --git a/src/signal_processing.rs b/src/signal_processing.rs new file mode 100644 index 0000000..3d946c3 --- /dev/null +++ b/src/signal_processing.rs @@ -0,0 +1,114 @@ +// vim: noet + +use fftw::array::AlignedVec; +use fftw::plan::*; +use fftw::types::*; +use std::f32::consts::PI; + +pub struct SignalProcessing +{ + samp_rate: f32, + + fft_window: Vec, + + fft_input: AlignedVec, + fft_output: AlignedVec, + + fft_plan: R2CPlan32, + + fft_absolute: Vec, +} + +impl SignalProcessing +{ + fn hann_window(block_size: usize) -> Vec + { + let mut window = vec![0.0; block_size]; + + for i in 0..block_size { + window[i] = (PI * (i as f32) / (block_size as f32)).sin().powi(2); + } + + window + } + + pub fn new(block_size: usize, samp_rate: f32) -> fftw::error::Result + { + let freq_domain_size = block_size/2 + 1; + + let s = SignalProcessing { + samp_rate: samp_rate, + fft_window: SignalProcessing::hann_window(block_size), + fft_input: AlignedVec::new(block_size), + fft_output: AlignedVec::new(freq_domain_size), + fft_plan: R2CPlan::aligned(&[block_size], Flag::MEASURE)?, + + fft_absolute: vec![0.0; freq_domain_size], + }; + + Ok(s) + } + + fn apply_window(&mut self) + { + self.fft_input.iter_mut() + .zip(self.fft_window.iter()) + .for_each(|(s, w)| *s *= w); + } + + pub fn import_i16_stereo(&mut self, data: &[i16]) -> std::result::Result<(), &str> + { + if data.len() != 2*self.fft_input.len() { + return Err("Stereo data length does not match 2x the FFT input length."); + } + + data.chunks_exact(2) + .map(|channels| (channels[0] as f32 + channels[1] as f32) / 2.0 / 32768.0) + .zip(self.fft_input.iter_mut()) + .for_each(|(c, t)| *t = c); + + self.apply_window(); + + Ok(()) + } + + pub fn import_i16_mono(&mut self, data: &[i16]) -> std::result::Result<(), &str> + { + if data.len() != self.fft_input.len() { + return Err("Mono data length does not match the FFT input length."); + } + + data.iter() + .map(|&sample| (sample as f32) / 32768.0) + .zip(self.fft_input.iter_mut()) + .for_each(|(c, t)| *t = c); + + self.apply_window(); + + Ok(()) + } + + pub fn update_fft(&mut self) -> fftw::error::Result<()> + { + self.fft_plan.r2c(&mut self.fft_input, &mut self.fft_output)?; + + for (i, abs_sample) in self.fft_absolute.iter_mut().enumerate() { + *abs_sample = self.fft_output[i].norm(); + } + + Ok(()) + } + + fn freq_to_idx(&self, freq: f32) -> usize + { + (freq * (self.fft_input.len() as f32) / self.samp_rate) as usize + } + + pub fn get_energy_in_band(&self, freq_start: f32, freq_end: f32) -> f32 + { + let start_bin = self.freq_to_idx(freq_start); + let end_bin = self.freq_to_idx(freq_end); + + self.fft_absolute[start_bin ..= end_bin].iter().sum() + } +}