diff --git a/beat_detection/Cargo.toml b/beat_detection/Cargo.toml index 6d41ca0..fb42b62 100644 --- a/beat_detection/Cargo.toml +++ b/beat_detection/Cargo.toml @@ -14,3 +14,4 @@ ringbuffer = "0.8.3" num = "0.4.0" serialport = "4.0.1" palette = "0.6.0" +rand = "0.8.4" diff --git a/beat_detection/src/dmx_controller.rs b/beat_detection/src/dmx_controller.rs index 7a3d92a..e8a464b 100644 --- a/beat_detection/src/dmx_controller.rs +++ b/beat_detection/src/dmx_controller.rs @@ -1,4 +1,9 @@ -use std::{io, sync::{Arc, Mutex}, thread, time::{Duration, Instant}}; +use std::{ + io, + sync::{Arc, Mutex}, + thread, + time::{Duration, Instant}, +}; use anyhow::{anyhow, Result}; use serialport::SerialPort; @@ -11,7 +16,9 @@ enum MCUResponse { Sync, Ack, #[allow(dead_code)] - Info { num_pkts: u32 }, + Info { + num_pkts: u32, + }, } fn poll_response(ser: &mut dyn SerialPort) -> Result { @@ -38,9 +45,12 @@ fn poll_response(ser: &mut dyn SerialPort) -> Result { } } -pub fn controller_thread(running: Arc>, movingheads: Arc>) -> Result<()> { +pub fn controller_thread( + running: Arc>, + movingheads: Arc>, +) -> Result<()> { let frame_time = Duration::from_secs_f64(1.0 / FPS as f64); - + let mut dmx_buffer = [0u8; 512]; let mut ser = serialport::new("/dev/ttyUSB0", 500_000) diff --git a/beat_detection/src/fixtures.rs b/beat_detection/src/fixtures.rs index 74d2228..baf79cb 100644 --- a/beat_detection/src/fixtures.rs +++ b/beat_detection/src/fixtures.rs @@ -45,20 +45,7 @@ impl DMXFixture for MovingHead { let offset = self.start_addr - 1; let channels = [ - pan, - pan_fine, - tilt, - tilt_fine, - self.speed, - dimmer, - r, - g, - b, - w, - 0, - 0, - 0, - 0, + pan, pan_fine, tilt, tilt_fine, self.speed, dimmer, r, g, b, w, 0, 0, 0, 0, ]; dst[offset..offset + channels.len()].copy_from_slice(&channels); diff --git a/beat_detection/src/layers/constant_brightness.rs b/beat_detection/src/layers/constant_brightness.rs new file mode 100644 index 0000000..585361a --- /dev/null +++ b/beat_detection/src/layers/constant_brightness.rs @@ -0,0 +1,24 @@ +use super::OutputLayer; + +pub struct ConstantBrightness { + pub brightness: f32, +} + +impl OutputLayer for ConstantBrightness { + fn tick(&mut self, _dt: std::time::Duration) {} + + fn draw_sdl( + &self, + _canvas: &mut sdl2::render::Canvas, + _texture_size: u32, + ) { + } + + fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {} + + fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) { + for head in output.iter_mut() { + head.dimmer = self.brightness; + } + } +} diff --git a/beat_detection/src/layers/hsl_cycle.rs b/beat_detection/src/layers/hsl_cycle.rs index c61b112..662baab 100644 --- a/beat_detection/src/layers/hsl_cycle.rs +++ b/beat_detection/src/layers/hsl_cycle.rs @@ -32,7 +32,7 @@ impl OutputLayer for HSLCycle { 0.5, ) .into_color(); - + self.rgb = hsl.into_format().into_raw(); } @@ -42,9 +42,7 @@ impl OutputLayer for HSLCycle { canvas.fill_rect(Rect::new(0, 0, 5, 5)).unwrap(); } - fn on_sdl_event(&mut self, _event: &sdl2::event::Event) { - - } + fn on_sdl_event(&mut self, _event: &sdl2::event::Event) {} fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) { let [r, g, b] = self.rgb; diff --git a/beat_detection/src/layers/mod.rs b/beat_detection/src/layers/mod.rs index ff41c25..a31682b 100644 --- a/beat_detection/src/layers/mod.rs +++ b/beat_detection/src/layers/mod.rs @@ -1,5 +1,8 @@ -pub mod hsl_cycle; pub mod beat_detector; +pub mod constant_brightness; +pub mod hsl_cycle; +pub mod movement; +pub mod tap; use std::time::Duration; @@ -11,8 +14,8 @@ pub trait OutputLayer { fn tick(&mut self, dt: Duration); fn draw_sdl(&self, canvas: &mut Canvas, texture_size: u32); - + fn on_sdl_event(&mut self, event: &Event); fn update_dmx(&self, output: &mut [MovingHead; 4]); -} \ No newline at end of file +} diff --git a/beat_detection/src/layers/movement.rs b/beat_detection/src/layers/movement.rs new file mode 100644 index 0000000..ad458e4 --- /dev/null +++ b/beat_detection/src/layers/movement.rs @@ -0,0 +1,78 @@ +use std::{ + f32::consts::PI, + time::{Duration, Instant}, +}; + +use rand::random; +use sdl2::{event::Event, keyboard::Keycode}; + +use crate::util::rescale; + +use super::OutputLayer; + +pub struct HeadMover { + movement_duration: Duration, + last_timestamp: Instant, + targets: [(f32, f32); 4], +} + +impl HeadMover { + pub fn new(movement_duration: Duration) -> Self { + Self { + movement_duration, + last_timestamp: Instant::now() - movement_duration, + targets: Default::default(), + } + } +} + +fn sample_random_point() -> (f32, f32) { + let pan = rescale(random::(), (0.0, 1.0), (-0.5 * PI, 0.5 * PI)); + let tilt = random::().acos(); + + (pan, tilt) +} + +impl OutputLayer for HeadMover { + fn tick(&mut self, _dt: Duration) { + let now = Instant::now(); + if now - self.last_timestamp >= self.movement_duration { + for target in self.targets.iter_mut() { + *target = sample_random_point(); + } + self.last_timestamp = now; + } + } + + fn draw_sdl( + &self, + _canvas: &mut sdl2::render::Canvas, + _texture_size: u32, + ) { + } + + fn on_sdl_event(&mut self, event: &sdl2::event::Event) { + match event { + Event::KeyDown { + keycode: Some(Keycode::Return), + .. + } => { + let now = Instant::now(); + for target in self.targets.iter_mut() { + *target = sample_random_point(); + } + self.last_timestamp = now; + } + + _ => {} + } + } + + fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) { + for (head, target) in output.iter_mut().zip(self.targets.iter()) { + let (pan, tilt) = target; + head.pan = *pan; + head.tilt = *tilt; + } + } +} diff --git a/beat_detection/src/layers/tap.rs b/beat_detection/src/layers/tap.rs new file mode 100644 index 0000000..1a5cf59 --- /dev/null +++ b/beat_detection/src/layers/tap.rs @@ -0,0 +1,102 @@ +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +use sdl2::{event::Event, keyboard::Keycode, pixels::Color}; + +use super::OutputLayer; + +pub struct TapMetronome { + max_time: Duration, + timestamps: VecDeque, + seconds_per_beat: Option, + brightness: f32, + decay: f32, +} + +impl TapMetronome { + pub fn new(max_time: Duration) -> Self { + Self { + max_time, + timestamps: VecDeque::new(), + seconds_per_beat: None, + brightness: 0.0, + decay: 1.0, + } + } +} + +impl OutputLayer for TapMetronome { + fn tick(&mut self, _dt: std::time::Duration) { + let now = Instant::now(); + if let (Some(stamp), Some(spb)) = (self.timestamps.back(), self.seconds_per_beat) { + let dt = (now - *stamp).as_secs_f32(); + + let beat_offset = dt % spb; + + self.brightness = (-1.0 * self.decay * beat_offset).exp() + } else { + self.brightness = 0.0; + } + } + + fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas, _texture_size: u32) { + let v = (255.0 * self.brightness) as u8; + canvas.set_draw_color(Color::RGB(v, v, v)); + canvas.clear(); + } + + fn on_sdl_event(&mut self, event: &sdl2::event::Event) { + match event { + Event::KeyDown { + keycode: Some(Keycode::Space), + .. + } => { + let now = Instant::now(); + while let Some(stamp) = self.timestamps.front() { + if now - *stamp < self.max_time { + break; + } + self.timestamps.pop_front(); + } + self.timestamps.push_back(now); + + if self.timestamps.len() >= 2 { + let dt = *self.timestamps.back().unwrap() - *self.timestamps.front().unwrap(); + let spb = dt.as_secs_f32() / (self.timestamps.len() - 1) as f32; + + println!("Detected BPM: {:.2}", 60.0 / spb); + + self.seconds_per_beat = Some(spb); + } else { + self.seconds_per_beat = None; + } + } + + Event::KeyDown { + keycode: Some(Keycode::Down), + .. + } => { + self.decay += 0.1; + println!("Decay: {:.3}", self.decay) + } + + Event::KeyDown { + keycode: Some(Keycode::Up), + .. + } => { + self.decay -= 0.1; + println!("Decay: {:.3}", self.decay) + } + + _ => {} + } + } + + fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) { + for head in output.iter_mut() { + head.dimmer = 0.2 + 0.8 * self.brightness; + } + } +} diff --git a/beat_detection/src/main.rs b/beat_detection/src/main.rs index 0433ead..96226f3 100644 --- a/beat_detection/src/main.rs +++ b/beat_detection/src/main.rs @@ -5,10 +5,17 @@ mod fixtures; mod layers; mod util; -use std::{sync::{Arc, Mutex}, thread, time::Duration}; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; use anyhow::Result; -use layers::{OutputLayer, beat_detector::BeatDetectinator, hsl_cycle::HSLCycle}; +use layers::{ + beat_detector::BeatDetectinator, constant_brightness::ConstantBrightness, hsl_cycle::HSLCycle, + movement::HeadMover, tap::TapMetronome, OutputLayer, +}; use sdl2::{event::Event, keyboard::Keycode, pixels::Color, rect::Rect}; use crate::dmx_controller::controller_thread; @@ -16,6 +23,12 @@ use crate::fixtures::MovingHead; const FPS: usize = 50; +enum BrightnessControl { + BeatDetection, + TapMetronome, + ConstantBrightness, +} + fn main() -> Result<()> { // dmx thread @@ -36,7 +49,13 @@ fn main() -> Result<()> { // output layers let mut beat_detector = BeatDetectinator::new(); + let mut tap_metronome = TapMetronome::new(Duration::from_secs(2)); + let mut constant_brightness = ConstantBrightness { brightness: 1.0 }; + + let mut active_brightness_control = BrightnessControl::BeatDetection; + let mut hsl_cycle = HSLCycle::new(Duration::from_secs(5)); + let mut head_mover = HeadMover::new(Duration::from_secs_f32(0.5)); // sdl @@ -55,7 +74,7 @@ fn main() -> Result<()> { texture_size, texture_size, )?; - + let frame_duration = Duration::from_secs_f64(1.0 / FPS as f64); let mut event_pump = sdl.event_pump().unwrap(); @@ -69,32 +88,77 @@ fn main() -> Result<()> { .. } => break 'running, + Event::KeyDown { + keycode: Some(Keycode::F1), + .. + } => { + active_brightness_control = BrightnessControl::BeatDetection; + println!("Using automatic beat detection.") + } + + Event::KeyDown { + keycode: Some(Keycode::F2), + .. + } => { + active_brightness_control = BrightnessControl::TapMetronome; + println!("Using tap metronome.") + } + + Event::KeyDown { + keycode: Some(Keycode::F3), + .. + } => { + active_brightness_control = BrightnessControl::ConstantBrightness; + println!("Using constant brightness.") + } + event => { - // println!("{:?}", event); - beat_detector.on_sdl_event(&event); + match active_brightness_control { + BrightnessControl::BeatDetection => beat_detector.on_sdl_event(&event), + BrightnessControl::TapMetronome => tap_metronome.on_sdl_event(&event), + BrightnessControl::ConstantBrightness => { + constant_brightness.on_sdl_event(&event) + } + } hsl_cycle.on_sdl_event(&event); + head_mover.on_sdl_event(&event); } } } - + beat_detector.tick(frame_duration); + tap_metronome.tick(frame_duration); hsl_cycle.tick(frame_duration); - + head_mover.tick(frame_duration); + { let mut output = movingheads.lock().unwrap(); - beat_detector.update_dmx(&mut *output); + match active_brightness_control { + BrightnessControl::BeatDetection => beat_detector.update_dmx(&mut *output), + BrightnessControl::TapMetronome => tap_metronome.update_dmx(&mut *output), + BrightnessControl::ConstantBrightness => { + constant_brightness.update_dmx(&mut *output) + } + } hsl_cycle.update_dmx(&mut *output); + head_mover.update_dmx(&mut *output); } canvas.set_draw_color(Color::RGB(0, 0, 0)); canvas.clear(); canvas.with_texture_canvas(&mut texture, |canvas| { - beat_detector.draw_sdl(canvas, texture_size); + match active_brightness_control { + BrightnessControl::BeatDetection => beat_detector.draw_sdl(canvas, texture_size), + BrightnessControl::TapMetronome => tap_metronome.draw_sdl(canvas, texture_size), + BrightnessControl::ConstantBrightness => { + constant_brightness.draw_sdl(canvas, texture_size) + } + } hsl_cycle.draw_sdl(canvas, texture_size); + head_mover.draw_sdl(canvas, texture_size); })?; - let (w, h) = canvas.window().drawable_size(); let target_rect = if w > h {