Implement beat detection -> dmx output
This commit is contained in:
parent
b0fecde639
commit
9f07172f60
@ -11,3 +11,6 @@ psimple = { version = "2.23.0", package = "libpulse-simple-binding" }
|
|||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
sdl2 = "0.35.1"
|
sdl2 = "0.35.1"
|
||||||
ringbuffer = "0.8.3"
|
ringbuffer = "0.8.3"
|
||||||
|
num = "0.4.0"
|
||||||
|
serialport = "4.0.1"
|
||||||
|
palette = "0.6.0"
|
||||||
|
118
beat_detection/src/dmx_controller.rs
Normal file
118
beat_detection/src/dmx_controller.rs
Normal file
@ -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<MCUResponse> {
|
||||||
|
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<Mutex<bool>>, brightness: Arc<Mutex<f32>>) -> 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
beat_detection/src/fixtures.rs
Normal file
66
beat_detection/src/fixtures.rs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,10 @@
|
|||||||
mod capture;
|
mod capture;
|
||||||
mod dsp;
|
mod dsp;
|
||||||
|
mod fixtures;
|
||||||
|
mod util;
|
||||||
|
mod dmx_controller;
|
||||||
|
|
||||||
|
use std::{sync::{Arc, Mutex}, thread};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pulse::sample::{Format, Spec};
|
use pulse::sample::{Format, Spec};
|
||||||
@ -11,7 +16,7 @@ use sdl2::{
|
|||||||
rect::{Point, Rect},
|
rect::{Point, Rect},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::dsp::ZTransformFilter;
|
use crate::{dmx_controller::controller_thread, dsp::ZTransformFilter};
|
||||||
|
|
||||||
const SAMPLE_RATE: usize = 5000;
|
const SAMPLE_RATE: usize = 5000;
|
||||||
const FPS: usize = 50;
|
const FPS: usize = 50;
|
||||||
@ -31,6 +36,15 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
assert!(spec.is_valid());
|
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 reader = capture::get_audio_reader(&spec)?;
|
||||||
let mut buffer = [0u8; 4 * BUFFER_SIZE];
|
let mut buffer = [0u8; 4 * BUFFER_SIZE];
|
||||||
|
|
||||||
@ -41,10 +55,6 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let mut threshold = 1.5f32;
|
let mut threshold = 1.5f32;
|
||||||
|
|
||||||
let mut brightness = 0f32;
|
|
||||||
|
|
||||||
let mut beat_toggle = false;
|
|
||||||
|
|
||||||
// sdl
|
// sdl
|
||||||
|
|
||||||
let sdl = sdl2::init().unwrap();
|
let sdl = sdl2::init().unwrap();
|
||||||
@ -90,6 +100,13 @@ fn main() -> Result<()> {
|
|||||||
threshold -= 0.01f32;
|
threshold -= 0.01f32;
|
||||||
println!("threshold: {:.2}", threshold);
|
println!("threshold: {:.2}", threshold);
|
||||||
}
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Space),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut brightness = brightness.lock().unwrap();
|
||||||
|
*brightness = 1.0;
|
||||||
|
}
|
||||||
Event::KeyDown {
|
Event::KeyDown {
|
||||||
keycode: Some(k), ..
|
keycode: Some(k), ..
|
||||||
} => {
|
} => {
|
||||||
@ -139,21 +156,18 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// background
|
// background
|
||||||
|
let v;
|
||||||
let prev_brightness = brightness;
|
{
|
||||||
brightness = if beat > threshold {
|
let mut brightness = brightness.lock().unwrap();
|
||||||
|
*brightness = if beat > threshold {
|
||||||
1f32
|
1f32
|
||||||
} else {
|
} else {
|
||||||
0.75f32 * brightness
|
0.75f32 * *brightness
|
||||||
};
|
};
|
||||||
|
|
||||||
if brightness - prev_brightness > 0.5 {
|
v = (255f32 * *brightness) as u8;
|
||||||
println!("beat. {}", if beat_toggle {"-"} else {"|"});
|
|
||||||
beat_toggle ^= true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let v = (255f32 * brightness) as u8;
|
|
||||||
|
|
||||||
canvas.set_draw_color(Color::RGB(v, v, v));
|
canvas.set_draw_color(Color::RGB(v, v, v));
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
|
|
||||||
@ -254,5 +268,12 @@ fn main() -> Result<()> {
|
|||||||
canvas.present();
|
canvas.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut dmx_running = dmx_running.lock().unwrap();
|
||||||
|
*dmx_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dmx_thread.join().unwrap()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
8
beat_detection/src/util.rs
Normal file
8
beat_detection/src/util.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use num::traits::float::Float;
|
||||||
|
|
||||||
|
pub fn rescale<T: Float>(x: T, from: (T, T), to: (T, T)) -> T {
|
||||||
|
let (a, b) = from;
|
||||||
|
let (c, d) = to;
|
||||||
|
|
||||||
|
c + (d - c) * (x - a) / (b - a)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user