This commit is contained in:
2021-10-28 01:31:19 +02:00
parent 9f07172f60
commit 30a4f83d32
5 changed files with 413 additions and 219 deletions

View File

@@ -0,0 +1,288 @@
use std::{
sync::{Arc, Mutex},
thread::{self, JoinHandle},
};
use anyhow::Result;
use pulse::sample::{Format, Spec};
use ringbuffer::{ConstGenericRingBuffer, RingBufferExt, RingBufferWrite};
use sdl2::{
event::Event,
keyboard::Keycode,
pixels::Color,
rect::{Point, Rect},
};
use crate::{
capture,
dsp::{self, ZTransformFilter},
};
use super::OutputLayer;
const SAMPLE_RATE: usize = 5000;
const PULSE_UPDATES_PER_SECOND: usize = 50;
const BUFFER_SIZE: usize = SAMPLE_RATE / PULSE_UPDATES_PER_SECOND;
const POINT_COUNT: usize = SAMPLE_RATE / 200;
const POINT_BUFFER_SIZE: usize = POINT_COUNT.next_power_of_two();
const MIN_MAX_SAMPLE_COUNT: usize = 10;
pub struct BeatDetectinator {
join_handle: Option<JoinHandle<Result<()>>>,
shared_state: Arc<Mutex<BeatDetectinatorSharedState>>,
}
struct BeatDetectinatorSharedState {
running: bool,
brightness: f32,
threshold: f32,
point_buf: ConstGenericRingBuffer<f32, POINT_BUFFER_SIZE>,
}
impl BeatDetectinator {
pub fn new() -> Self {
let shared_state = Arc::new(Mutex::new(BeatDetectinatorSharedState {
running: true,
brightness: 0.0,
threshold: 1.5,
point_buf: Default::default(),
}));
{
shared_state.lock().unwrap().point_buf.fill_default();
}
let join_handle = {
let shared_state = shared_state.clone();
Some(thread::spawn(move || audio_loop(shared_state)))
};
println!("Audio thread started.");
Self {
join_handle,
shared_state,
}
}
}
impl Drop for BeatDetectinator {
fn drop(&mut self) {
{
self.shared_state.lock().unwrap().running = false;
}
match self.join_handle.take().unwrap().join().unwrap() {
Ok(_) => println!("Audio thread stopped."),
Err(e) => println!("Audio thread died: {:?}", e),
}
}
}
fn audio_loop(shared_state: Arc<Mutex<BeatDetectinatorSharedState>>) -> Result<()> {
let spec = Spec {
format: Format::F32le,
rate: SAMPLE_RATE as u32,
channels: 1,
};
assert!(spec.is_valid());
let reader = capture::get_audio_reader(&spec)?;
let mut buffer = [0u8; 4 * BUFFER_SIZE];
let mut bass_filter = dsp::BassFilter::default();
let mut envelope_filter = dsp::EnvelopeFilter::default();
let mut beat_filter = dsp::BeatFilter::default();
let mut j = 0;
loop {
{
if shared_state.lock().unwrap().running == false {
break Ok(());
}
}
reader.read(&mut buffer)?;
for i in 0..BUFFER_SIZE {
let mut float_bytes = [0u8; 4];
float_bytes.copy_from_slice(&buffer[4 * i..4 * i + 4]);
j += 1;
let sample = f32::from_le_bytes(float_bytes);
let mut value = bass_filter.process(sample);
if value < 0f32 {
value = -value;
}
let envelope = envelope_filter.process(value);
if j == 200 {
let beat = beat_filter.process(envelope);
shared_state.lock().unwrap().point_buf.push(beat);
j = 0;
}
}
}
}
impl OutputLayer for BeatDetectinator {
fn tick(&mut self, _dt: std::time::Duration) {}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, texture_size: u32) {
let min_y = 0f32;
let max_y = 3f32;
let beat = {
self.shared_state
.lock()
.unwrap()
.point_buf
.back()
.unwrap()
.clone()
};
let get_y = |y: &f32| {
let mut y = y.clone();
if y <= 0f32 {
y = 0f32;
}
y = (1f32 + y).log2();
let y = (y - min_y) / (max_y - min_y);
((1f32 - y) * texture_size as f32) as u32
};
// background
let v;
let (threshold, points) = {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.brightness = if beat > shared_state.threshold {
1f32
} else {
0.75f32 * shared_state.brightness
};
v = (255f32 * shared_state.brightness) as u8;
(
shared_state.threshold.clone(),
shared_state.point_buf.to_vec(),
)
};
canvas.set_draw_color(Color::RGB(v, v, v));
canvas.clear();
// zero
canvas.set_draw_color(Color::RGB(0, 128, 0));
let y = get_y(&0f32);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// min / max lines
canvas.set_draw_color(Color::RGB(255, 0, 0));
let min_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a < b { a } else { b })
.unwrap();
let x = texture_size - MIN_MAX_SAMPLE_COUNT as u32 * 10;
let y = get_y(min_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
let max_beat = points
.iter()
.skip(points.len() - MIN_MAX_SAMPLE_COUNT)
.reduce(|a, b| if a > b { a } else { b })
.unwrap();
let y = get_y(max_beat);
canvas
.draw_line(
Point::new(x as i32, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// threshhold line
canvas.set_draw_color(Color::RGB(0, 0, 255));
let y = get_y(&threshold);
canvas
.draw_line(
Point::new(0, y as i32),
Point::new(texture_size as i32, y as i32),
)
.unwrap();
// values
canvas.set_draw_color(Color::RGB(0, 255, 0));
for (i, beat) in points.iter().skip(points.len() - POINT_COUNT).enumerate() {
let x = 10 * i;
let y = get_y(beat);
canvas
.fill_rect(Rect::new((x + 1) as i32, (y - 1) as i32, 8, 3))
.unwrap();
}
}
fn on_sdl_event(&mut self, event: &Event) {
match event {
Event::KeyDown {
keycode: Some(Keycode::Up),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold += 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
Event::KeyDown {
keycode: Some(Keycode::Down),
..
} => {
let mut shared_state = self.shared_state.lock().unwrap();
shared_state.threshold -= 0.01f32;
println!("threshold: {:.2}", shared_state.threshold);
}
_ => {}
}
}
fn update_dmx(&self, output: &mut [crate::fixtures::MovingHead; 4]) {
let shared_state = self.shared_state.lock().unwrap();
for head in output.iter_mut() {
head.dimmer = 0.2 + 0.8 * shared_state.brightness;
}
}
}

View File

@@ -0,0 +1,55 @@
use std::time::Duration;
use palette::{Hsl, IntoColor, Pixel, Srgb};
use sdl2::{pixels::Color, rect::Rect};
use crate::layers::OutputLayer;
pub struct HSLCycle {
cycle_millis: usize,
time: usize,
rgb: [u8; 3],
}
impl HSLCycle {
pub fn new(cycle_length: Duration) -> Self {
Self {
cycle_millis: cycle_length.as_millis() as usize,
time: 0,
rgb: [0, 0, 0],
}
}
}
impl OutputLayer for HSLCycle {
fn tick(&mut self, dt: std::time::Duration) {
self.time += dt.as_millis() as usize;
self.time %= self.cycle_millis;
let hsl: Srgb = Hsl::new(
360.0 * (self.time as f32 / self.cycle_millis as f32),
1.0,
0.5,
)
.into_color();
self.rgb = hsl.into_format().into_raw();
}
fn draw_sdl(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>, _texture_size: u32) {
let [r, g, b] = self.rgb;
canvas.set_draw_color(Color::RGB(r, g, b));
canvas.fill_rect(Rect::new(0, 0, 5, 5)).unwrap();
}
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;
for head in output.iter_mut() {
head.rgbw = (r, g, b, 0);
}
}
}

View File

@@ -0,0 +1,18 @@
pub mod hsl_cycle;
pub mod beat_detector;
use std::time::Duration;
use sdl2::{event::Event, render::Canvas, video::Window};
use crate::fixtures::MovingHead;
pub trait OutputLayer {
fn tick(&mut self, dt: Duration);
fn draw_sdl(&self, canvas: &mut Canvas<Window>, texture_size: u32);
fn on_sdl_event(&mut self, event: &Event);
fn update_dmx(&self, output: &mut [MovingHead; 4]);
}