From 7929861ad4c6759bebe13b0eafa297d79b0b0560 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Sun, 7 Mar 2021 20:32:34 +0100 Subject: [PATCH] Implement "particles" animation in Rust --- Cargo.lock | 64 ++++++++++++ Cargo.toml | 1 + src/animation.rs | 257 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 39 ++++--- src/userscript.rs | 158 ---------------------------- test.lua | 31 ------ 6 files changed, 349 insertions(+), 201 deletions(-) create mode 100644 src/animation.rs delete mode 100644 src/userscript.rs delete mode 100644 test.lua diff --git a/Cargo.lock b/Cargo.lock index bbe9a81..fe95260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,17 @@ dependencies = [ "num-complex", ] +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.23.0" @@ -193,6 +204,7 @@ dependencies = [ "byteorder", "fftw", "mlua", + "rand", ] [[package]] @@ -238,6 +250,12 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + [[package]] name = "proc-macro2" version = "1.0.24" @@ -256,6 +274,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core", +] + [[package]] name = "rawpointer" version = "0.1.0" @@ -296,3 +354,9 @@ name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" diff --git a/Cargo.toml b/Cargo.toml index 2de235c..b965389 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ edition = "2018" byteorder = "1.4" fftw = { version = "0.6", default-features = false, features = ["system"] } mlua = { version = "0.5", features = ["lua53"] } +rand = "0.8" diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 0000000..eb8bdd2 --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,257 @@ +// vim: noet + +use std::fmt; +use std::error::Error as StdError; + +use std::rc::Rc; +use std::cell::RefCell; + +use crate::config; +use crate::signal_processing::SignalProcessing; + +type Result = std::result::Result; + +/////////// Error Type and Implementation //////////// + +#[derive(Debug)] +pub enum AnimationError +{ + LuaError(mlua::Error), + ErrorMessage(std::string::String), +} + +impl fmt::Display for AnimationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AnimationError::LuaError(e) => f.write_fmt(format_args!("=== Lua Error ===\n{}\n=================", e))?, + AnimationError::ErrorMessage(s) => f.write_fmt(format_args!("Message({})", s))?, + }; + + Ok(()) + } +} + +impl StdError for AnimationError { + fn description(&self) -> &str { + match *self { + AnimationError::LuaError(_) => "Lua Error", + AnimationError::ErrorMessage(_) => "Error Message", + } + } +} + +/////////// Helper Structs //////////// + +#[derive(Copy, Clone)] +pub struct Color +{ + pub r: f32, + pub g: f32, + pub b: f32, + pub w: f32 +} + +impl Color +{ + pub fn scale(&mut self, factor: f32) + { + self.r *= factor; + self.g *= factor; + self.b *= factor; + self.w *= factor; + } + + fn _limit_component(c: &mut f32) + { + if *c > 1.0 { + *c = 1.0; + } else if *c < 0.0 { + *c = 0.0; + } + } + + pub fn limit(&mut self) + { + Color::_limit_component(&mut self.r); + Color::_limit_component(&mut self.g); + Color::_limit_component(&mut self.b); + Color::_limit_component(&mut self.w); + } + + pub fn ref_by_index_mut(&mut self, i: usize) -> Option<&mut f32> + { + match i { + 0 => Some(&mut self.r), + 1 => Some(&mut self.g), + 2 => Some(&mut self.b), + 3 => Some(&mut self.w), + _ => None + } + } + + pub fn ref_by_index(&self, i: usize) -> Option<&f32> + { + match i { + 0 => Some(&self.r), + 1 => Some(&self.g), + 2 => Some(&self.b), + 3 => Some(&self.w), + _ => None + } + } +} + +/////////// Animation Trait //////////// + +pub trait Animation { + fn new(sigproc: Rc>) -> Self; + + fn init(&mut self) -> Result<()>; + fn periodic(&mut self) -> Result<()>; + + fn get_colorlist(&self) -> &[ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS]; +} + +/////////// Animation implementations //////////// + +pub mod particles +{ + use crate::animation::{Color, Animation, Result}; + use crate::signal_processing::SignalProcessing; + use crate::config; + + use std::rc::Rc; + use std::cell::RefCell; + + use rand::Rng; + + const COOLDOWN_FACTOR : f32 = 0.99995; + const RGB_EXPONENT : f32 = 1.8; + const W_EXPONENT : f32 = 2.2; + const FADE_FACTOR : f32 = 0.98; + const AVG_LEDS_ACTIVATED : f32 = 0.05; + const WHITE_EXTRA_SCALE : f32 = 0.5; + + pub struct Particles + { + energy : [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + max_energy : Color, + + colorlists : [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + + sigproc: Rc>, + } + + impl Animation for Particles + { + fn new(sigproc: Rc>) -> Particles + { + Particles { + energy: [ [Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + max_energy: Color{r: 1.0, g: 1.0, b: 1.0, w: 1.0}, + colorlists: [ [Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + sigproc: sigproc, + } + } + + fn init(&mut self) -> Result<()> + { + Ok(()) + } + + fn periodic(&mut self) -> Result<()> + { + let sigproc = self.sigproc.borrow(); + + // extract frequency band energies + let cur_energy = Color{ + r: sigproc.get_energy_in_band( 0.0, 400.0), + g: sigproc.get_energy_in_band( 400.0, 4000.0), + b: sigproc.get_energy_in_band( 4000.0, 12000.0), + w: sigproc.get_energy_in_band(12000.0, 22000.0)}; + + // track the maximum energy with cooldown + self.max_energy.r *= COOLDOWN_FACTOR; + if cur_energy.r > self.max_energy.r { + self.max_energy.r = cur_energy.r; + } + + self.max_energy.g *= COOLDOWN_FACTOR; + if cur_energy.g > self.max_energy.g { + self.max_energy.g = cur_energy.g; + } + + self.max_energy.b *= COOLDOWN_FACTOR; + if cur_energy.b > self.max_energy.b { + self.max_energy.b = cur_energy.b; + } + + self.max_energy.w *= COOLDOWN_FACTOR; + if cur_energy.w > self.max_energy.w { + self.max_energy.w = cur_energy.w; + } + + // fade all LEDs towards black + for strip in 0..config::NUM_STRIPS { + for led in 0..config::NUM_LEDS_PER_STRIP { + self.energy[strip][led].scale(FADE_FACTOR); + } + } + + // distribute the energy for each color + let new_energy = Color{ + r: (cur_energy.r / self.max_energy.r).powf(RGB_EXPONENT), + g: (cur_energy.g / self.max_energy.g).powf(RGB_EXPONENT), + b: (cur_energy.b / self.max_energy.b).powf(RGB_EXPONENT), + w: (cur_energy.w / self.max_energy.w).powf(W_EXPONENT), + }; + + let mut remaining_energy = new_energy; + remaining_energy.scale(AVG_LEDS_ACTIVATED * config::NUM_LEDS_TOTAL as f32); + + let mut rng = rand::thread_rng(); + + // FIXME: how to call this code for green, blue and white as well without too much + // duplication? + for coloridx in 0..=3 { + let new_energy_ref = new_energy.ref_by_index(coloridx).unwrap(); + let rem_energy_ref = remaining_energy.ref_by_index_mut(coloridx).unwrap(); + + while *rem_energy_ref > 0.0 { + let mut rnd_energy = rng.gen::() * (*new_energy_ref) * 5.0; + + let rnd_strip = rng.gen_range(0..config::NUM_STRIPS); + let rnd_led = rng.gen_range(0..config::NUM_LEDS_PER_STRIP); + + if rnd_energy > *rem_energy_ref { + rnd_energy = *rem_energy_ref; + *rem_energy_ref = 0.0; + } else { + *rem_energy_ref -= rnd_energy; + } + + let led_ref = self.energy[rnd_strip][rnd_led].ref_by_index_mut(coloridx).unwrap(); + *led_ref += rnd_energy; + } + } + + // color post-processing + self.colorlists = self.energy; + + for strip in 0..config::NUM_STRIPS { + for led in 0..config::NUM_LEDS_PER_STRIP { + self.colorlists[strip][led].w *= WHITE_EXTRA_SCALE; + + self.colorlists[strip][led].limit(); + } + } + + Ok(()) + } + + fn get_colorlist(&self) -> &[ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS] + { + return &self.colorlists; + } + } +} diff --git a/src/main.rs b/src/main.rs index 6249f5c..eeec6c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,10 +9,12 @@ mod signal_processing; mod config; mod userscript; mod udpproto; +mod animation; use crate::signal_processing::SignalProcessing; -use crate::userscript::UserScript; +//use crate::userscript::UserScript; use crate::udpproto::UdpProto; +use crate::animation::Animation; use std::rc::Rc; use std::cell::RefCell; @@ -40,6 +42,7 @@ fn main() let sigproc = Rc::new(RefCell::new( SignalProcessing::new(config::BLOCK_LEN, config::SAMP_RATE).unwrap())); + /* // set up Lua environment println!("Loading user script..."); @@ -61,6 +64,15 @@ fn main() exit(1); } }; + */ + + println!("Contructing Animation..."); + + let mut anim: animation::particles::Particles = animation::Animation::new(sigproc.clone()); + + println!("Calling Animation::init()..."); + + anim.init().unwrap(); println!("Done! Starting main loop…"); @@ -112,24 +124,27 @@ fn main() } // call the periodic function in the user script - match script.periodic() { + match anim.periodic() { Ok(_) => (), Err(e) => { - println!("=== Lua Error ===\n{}\n====> Terminating.", e); + println!("=== Animation Error ===\n{}\n====> Terminating.", e); exit(1); } }; - // FIXME: only send with 60 FPS! - if Instant::now() > next_send_instant { - for i in 0..script.colorlists[0].len() { - udpproto.set_color((i / config::NUM_LEDS_PER_STRIP) as u8, - (i % config::NUM_LEDS_PER_STRIP) as u8, - (script.colorlists[0][i] * 255.0) as u8, - (script.colorlists[1][i] * 255.0) as u8, - (script.colorlists[2][i] * 255.0) as u8, - (script.colorlists[3][i] * 255.0) as u8).unwrap(); + let colorlists = anim.get_colorlist(); + + for i in 0..config::NUM_LEDS_TOTAL { + let strip = i / config::NUM_LEDS_PER_STRIP; + let led = i % config::NUM_LEDS_PER_STRIP; + + udpproto.set_color(strip as u8, + led as u8, + (colorlists[strip][led].r * 255.0) as u8, + (colorlists[strip][led].g * 255.0) as u8, + (colorlists[strip][led].b * 255.0) as u8, + (colorlists[strip][led].w * 255.0) as u8).unwrap(); } udpproto.commit().unwrap(); diff --git a/src/userscript.rs b/src/userscript.rs deleted file mode 100644 index be3a873..0000000 --- a/src/userscript.rs +++ /dev/null @@ -1,158 +0,0 @@ -// vim: noet - -/* - * Module for the Lua scripting interface of Musiclight. - */ - -/* Test code for reference - // 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()); -*/ - -use crate::config; -use crate::signal_processing::SignalProcessing; - -use mlua::Lua; -use mlua::FromLua; -use mlua::Error; - -use std::rc::Rc; -use std::cell::RefCell; - -pub struct UserScript -{ - lua_state: Lua, - - pub colorlists: [ [f32; config::NUM_LEDS_TOTAL]; 4], -} - -impl UserScript -{ - pub fn new(sigproc: Rc>, user_script_path: &str) -> std::result::Result - { - let s = UserScript { - lua_state: Lua::new(), - colorlists: [ [0f32; config::NUM_LEDS_TOTAL]; 4], - }; - - // provide some configuration constants to Lua via a table - let config_table = s.lua_state.create_table()?; - - config_table.set("sampling_rate", config::SAMP_RATE)?; - config_table.set("block_length", config::BLOCK_LEN)?; - config_table.set("samples_per_update", config::SAMPLES_PER_UPDATE)?; - - s.lua_state.globals().set("CONFIG", config_table)?; - - // register the signal processing reference as Lua user data - s.lua_state.globals().set("sigproc", SignalProcessingWrapper{ - signal_processing: sigproc - })?; - - // register the LED interface - s.lua_state.globals().set("leds", LuaLED{})?; - - // load the user script and execute it to make variables and functions available - let user_script = std::fs::read_to_string(user_script_path)?; - s.lua_state.load(&user_script).exec()?; - - Ok(s) - } - - pub fn init(&self) -> std::result::Result<(), mlua::Error> - { - // find the init functions - let lua_init_func: mlua::Function = self.lua_state.globals().get("init")?; - - lua_init_func.call( (config::NUM_STRIPS, config::NUM_LEDS_PER_STRIP) )?; - - Ok(()) - } - - pub fn periodic(&mut self) -> std::result::Result<(), mlua::Error> - { - // find the init functions - let lua_periodic_func: mlua::Function = self.lua_state.globals().get("periodic")?; - - // call the script's periodic() function, which (hopefully) returns four Tables with color - // values - let rvals = lua_periodic_func.call::<_, mlua::MultiValue>( () )?; - - // check the number of returned values - if rvals.len() != 4 { - return Err(Error::RuntimeError("Wrong number of return values from 'periodic'. Expected 4.".to_string())); - } - - // convert the Lua Tables to normal vectors - let mut i = 0; - for rval in rvals { - let table = mlua::Table::from_lua(rval, &self.lua_state)?; - - let v = table.sequence_values() - .map(|x| x.unwrap()) - .collect::>(); - - // check the length of the color array - if v.len() != config::NUM_LEDS_TOTAL { - return Err(Error::RuntimeError("Number of color values returned from 'periodic' must match number of LEDs given in 'init'.".to_string())); - } - - for j in 0 .. self.colorlists[i].len() { - self.colorlists[i][j] = v[j]; - } - - i += 1; - } - - Ok(()) - } -} - - -/* - * Wrap a SignalProcessing instance and provide a Lua interface for some of its methods. - */ -struct SignalProcessingWrapper -{ - signal_processing: Rc>, -} - -impl mlua::UserData for SignalProcessingWrapper -{ - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) - { - methods.add_method("get_energy_in_band", |_lua, this, (start_freq, end_freq): (f32, f32)| { - Ok(this.signal_processing.borrow().get_energy_in_band(start_freq, end_freq)) - }); - } -} - - -/* - * Lua interface for functions related to the LED setup - */ -struct LuaLED -{ -} - -impl mlua::UserData for LuaLED -{ - fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) - { - methods.add_method("idx", |_lua, _this, (strip, led): (usize, usize)| { - Ok( (strip - 1) * config::NUM_LEDS_PER_STRIP + (led - 1) + 1 ) - }); - } -} - diff --git a/test.lua b/test.lua deleted file mode 100644 index 1de3880..0000000 --- a/test.lua +++ /dev/null @@ -1,31 +0,0 @@ -function init(nstrip, nmod) - print("Initializing with "..nstrip.." strips with "..nmod.." modules each.") - - print("Sampling rate: "..CONFIG['sampling_rate'].." Hz") - - local nled = nstrip * nmod - - red = {} - green = {} - blue = {} - white = {} - - - for i = 1,nled do - red[i] = 0.8 - green[i] = 0.1 - blue[i] = 0.2 - white[i] = 0.1 - end - - return 0 -end - -function periodic() - bass = sigproc:get_energy_in_band(0, 400) - mid = sigproc:get_energy_in_band(400, 4000) - treble = sigproc:get_energy_in_band(4000, 20000) - - print("Bass: "..bass.." – Mid: "..mid.." – Treble: "..treble) - return red, green, blue, white -end