diff --git a/beat_detection/Cargo.toml b/beat_detection/Cargo.toml index 967b475..6d41ca0 100644 --- a/beat_detection/Cargo.toml +++ b/beat_detection/Cargo.toml @@ -11,3 +11,6 @@ psimple = { version = "2.23.0", package = "libpulse-simple-binding" } anyhow = "1.0.44" sdl2 = "0.35.1" ringbuffer = "0.8.3" +num = "0.4.0" +serialport = "4.0.1" +palette = "0.6.0" diff --git a/beat_detection/src/dmx_controller.rs b/beat_detection/src/dmx_controller.rs new file mode 100644 index 0000000..569f22b --- /dev/null +++ b/beat_detection/src/dmx_controller.rs @@ -0,0 +1,118 @@ +use std::{io, sync::{Arc, Mutex}, thread, time::{Duration, Instant}}; + +use anyhow::{anyhow, Result}; +use palette::{Hsl, IntoColor, Pixel, Srgb}; +use serialport::SerialPort; + +use crate::fixtures::{DMXFixture, MovingHead}; + +const FPS: u32 = 50; + +enum MCUResponse { + Sync, + Ack, + Info { num_pkts: u32 }, +} + +fn poll_response(ser: &mut dyn SerialPort) -> Result { + let mut read_buffer = vec![0u8; 32]; + let bytes_read; + loop { + match ser.read(read_buffer.as_mut_slice()) { + Ok(t) => { + bytes_read = t; + break; + } + Err(ref e) if e.kind() == io::ErrorKind::TimedOut => continue, + Err(e) => Err(e), + }? + } + + let response = std::str::from_utf8(&read_buffer[..bytes_read])?; + + match response.trim() { + "Sync." => Ok(MCUResponse::Sync), + "Ack." => Ok(MCUResponse::Ack), + s if s.starts_with("Info") => Ok(MCUResponse::Info { num_pkts: 69 }), + s => Err(anyhow!("Unknown response: \"{}\"", s)), + } +} + +pub fn controller_thread(running: Arc>, brightness: Arc>) -> Result<()> { + let frame_time = Duration::from_secs_f64(1.0 / FPS as f64); + + let hsl_cycle = 12 * FPS; + + let mut dmx_buffer = [0u8; 512]; + + let mut movinghead = MovingHead::new(1); + + let mut ser = serialport::new("/dev/ttyUSB0", 500_000) + .timeout(Duration::from_millis(10)) + .open()?; + + // wait for initial sync + loop { + match poll_response(&mut *ser) { + Ok(MCUResponse::Sync) => break, + _ => continue, + } + } + + let mut t = 0; + + 'main: loop { + { + let running = running.lock().unwrap(); + + if !*running { + break Ok(()); + } + } + + let loop_start = Instant::now(); + + let hsl: Srgb = Hsl::new(360.0 * (t as f32 / hsl_cycle as f32), 1.0, 0.5).into_color(); + + let [r, g, b]: [u8; 3] = hsl.into_format().into_raw(); + movinghead.rgbw = (r, g, b, 0); + + movinghead.dimmer = { + let brightness = brightness.lock().unwrap(); + + 0.2 + 0.8 * *brightness + }; + + movinghead.render(&mut dmx_buffer); + + t += 1; + t %= hsl_cycle; + + let write_result = ser.write(&dmx_buffer); + + if write_result.is_err() { + loop { + match poll_response(&mut *ser) { + Ok(MCUResponse::Sync) => continue 'main, + _ => continue, + } + } + } + + loop { + match poll_response(&mut *ser) { + Ok(MCUResponse::Ack) => break, + Ok(MCUResponse::Info { .. }) | Err(_) => continue, + Ok(MCUResponse::Sync) => continue 'main, + } + } + + let loop_time = loop_start.elapsed(); + + if loop_time < frame_time { + thread::sleep(frame_time - loop_time); + } else { + println!("loop took too long!"); + } + } +} diff --git a/beat_detection/src/fixtures.rs b/beat_detection/src/fixtures.rs new file mode 100644 index 0000000..74d2228 --- /dev/null +++ b/beat_detection/src/fixtures.rs @@ -0,0 +1,66 @@ +use std::f32::consts::{FRAC_PI_2, PI}; + +use crate::util::rescale; + +pub struct MovingHead { + start_addr: usize, + + pub pan: f32, // -3pi/2 to 3pi/2 + pub tilt: f32, // -pi/2 to pi/2 + pub dimmer: f32, // 0 to 1 + pub rgbw: (u8, u8, u8, u8), + pub speed: u8, // reversed +} + +impl MovingHead { + pub fn new(start_addr: usize) -> Self { + Self { + start_addr, + + pan: 0f32, + tilt: 0f32, + speed: 0u8, + dimmer: 0f32, + rgbw: (0u8, 0u8, 0u8, 0u8), + } + } +} + +pub trait DMXFixture { + fn render(&self, dst: &mut [u8]); +} + +impl DMXFixture for MovingHead { + fn render(&self, dst: &mut [u8]) { + let pan = rescale(self.pan, (-1.5 * PI, 1.5 * PI), (255.0, 0.0)) as u8; + let pan_fine = 0; + + let tilt = rescale(self.tilt, (-1.0 * FRAC_PI_2, FRAC_PI_2), (0.0, 255.0)) as u8; + let tilt_fine = 0; + + let dimmer = (7 + (127.0 * self.dimmer) as u8).clamp(7, 134); + + let (r, g, b, w) = self.rgbw; + + 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, + ]; + + dst[offset..offset + channels.len()].copy_from_slice(&channels); + } +} diff --git a/beat_detection/src/main.rs b/beat_detection/src/main.rs index 596d724..5c39384 100644 --- a/beat_detection/src/main.rs +++ b/beat_detection/src/main.rs @@ -1,5 +1,10 @@ mod capture; mod dsp; +mod fixtures; +mod util; +mod dmx_controller; + +use std::{sync::{Arc, Mutex}, thread}; use anyhow::Result; use pulse::sample::{Format, Spec}; @@ -11,7 +16,7 @@ use sdl2::{ rect::{Point, Rect}, }; -use crate::dsp::ZTransformFilter; +use crate::{dmx_controller::controller_thread, dsp::ZTransformFilter}; const SAMPLE_RATE: usize = 5000; const FPS: usize = 50; @@ -30,6 +35,15 @@ fn main() -> Result<()> { channels: 1, }; assert!(spec.is_valid()); + + let dmx_running = Arc::new(Mutex::new(true)); + let brightness = Arc::new(Mutex::new(0f32)); + + let dmx_thread = { + let dmx_running = dmx_running.clone(); + let brightness = brightness.clone(); + thread::spawn(move || controller_thread(dmx_running, brightness)) + }; let reader = capture::get_audio_reader(&spec)?; let mut buffer = [0u8; 4 * BUFFER_SIZE]; @@ -41,10 +55,6 @@ fn main() -> Result<()> { let mut threshold = 1.5f32; - let mut brightness = 0f32; - - let mut beat_toggle = false; - // sdl let sdl = sdl2::init().unwrap(); @@ -90,6 +100,13 @@ fn main() -> Result<()> { threshold -= 0.01f32; println!("threshold: {:.2}", threshold); } + Event::KeyDown { + keycode: Some(Keycode::Space), + .. + } => { + let mut brightness = brightness.lock().unwrap(); + *brightness = 1.0; + } Event::KeyDown { keycode: Some(k), .. } => { @@ -139,21 +156,18 @@ fn main() -> Result<()> { }; // background + let v; + { + let mut brightness = brightness.lock().unwrap(); + *brightness = if beat > threshold { + 1f32 + } else { + 0.75f32 * *brightness + }; - let prev_brightness = brightness; - brightness = if beat > threshold { - 1f32 - } else { - 0.75f32 * brightness - }; - - if brightness - prev_brightness > 0.5 { - println!("beat. {}", if beat_toggle {"-"} else {"|"}); - beat_toggle ^= true; + v = (255f32 * *brightness) as u8; } - let v = (255f32 * brightness) as u8; - canvas.set_draw_color(Color::RGB(v, v, v)); canvas.clear(); @@ -253,6 +267,13 @@ fn main() -> Result<()> { canvas.copy(&texture, None, Some(target_rect)).unwrap(); canvas.present(); } + + { + let mut dmx_running = dmx_running.lock().unwrap(); + *dmx_running = false; + } + + dmx_thread.join().unwrap()?; Ok(()) } diff --git a/beat_detection/src/util.rs b/beat_detection/src/util.rs new file mode 100644 index 0000000..523aab2 --- /dev/null +++ b/beat_detection/src/util.rs @@ -0,0 +1,8 @@ +use num::traits::float::Float; + +pub fn rescale(x: T, from: (T, T), to: (T, T)) -> T { + let (a, b) = from; + let (c, d) = to; + + c + (d - c) * (x - a) / (b - a) +}