Add beat detection script
This commit is contained in:
parent
49f306226b
commit
120d8d8c49
14
beat_detection/.gitignore
vendored
Normal file
14
beat_detection/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
13
beat_detection/Cargo.toml
Normal file
13
beat_detection/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "beat_detection"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pulse = { version = "2.23.1", package = "libpulse-binding" }
|
||||||
|
psimple = { version = "2.23.0", package = "libpulse-simple-binding" }
|
||||||
|
anyhow = "1.0.44"
|
||||||
|
sdl2 = "0.35.1"
|
||||||
|
ringbuffer = "0.8.3"
|
95
beat_detection/src/capture.rs
Normal file
95
beat_detection/src/capture.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use psimple::Simple;
|
||||||
|
use pulse::context::{Context, FlagSet as ContextFlagSet};
|
||||||
|
use pulse::mainloop::standard::{IterateResult, Mainloop};
|
||||||
|
use pulse::sample::{Format, Spec};
|
||||||
|
use pulse::stream::Direction;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Some manual poking around in PulseAudio to get the name of the default sink
|
||||||
|
*/
|
||||||
|
|
||||||
|
fn poll_mainloop(mainloop: &Rc<RefCell<Mainloop>>) -> Result<()> {
|
||||||
|
match mainloop.borrow_mut().iterate(true) {
|
||||||
|
IterateResult::Quit(_) | IterateResult::Err(_) => {
|
||||||
|
return Err(anyhow!("Iterate state was not success"));
|
||||||
|
}
|
||||||
|
IterateResult::Success(_) => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pulse_default_sink() -> Result<String> {
|
||||||
|
let mainloop = Rc::new(RefCell::new(
|
||||||
|
Mainloop::new().ok_or(anyhow!("Failed to create mainloop"))?,
|
||||||
|
));
|
||||||
|
let ctx = Rc::new(RefCell::new(
|
||||||
|
Context::new(mainloop.borrow().deref(), "gib_default_sink")
|
||||||
|
.ok_or(anyhow!("Failed to create context"))?,
|
||||||
|
));
|
||||||
|
|
||||||
|
ctx.borrow_mut()
|
||||||
|
.connect(None, ContextFlagSet::NOFLAGS, None)?;
|
||||||
|
|
||||||
|
// Wait for context to be ready
|
||||||
|
loop {
|
||||||
|
poll_mainloop(&mainloop)?;
|
||||||
|
|
||||||
|
match ctx.borrow().get_state() {
|
||||||
|
pulse::context::State::Ready => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pulse::context::State::Failed | pulse::context::State::Terminated => {
|
||||||
|
return Err(anyhow!("Context was in failed/terminated state"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = Rc::new(RefCell::new(None));
|
||||||
|
let cb_result_ref = result.clone();
|
||||||
|
|
||||||
|
ctx.borrow().introspect().get_server_info(move |info| {
|
||||||
|
*cb_result_ref.borrow_mut() = if let Some(ref sink_name) = info.default_sink_name {
|
||||||
|
Some(Ok(sink_name.to_string()))
|
||||||
|
} else {
|
||||||
|
Some(Err(()))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(result) = result.borrow().deref() {
|
||||||
|
return result
|
||||||
|
.to_owned()
|
||||||
|
.map_err(|_| anyhow!("Default sink name was empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
poll_mainloop(&mainloop)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get a PASimple instance which reads from the default sink monitor
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub fn get_audio_reader(spec: &Spec) -> Result<Simple> {
|
||||||
|
let mut default_sink_name = get_pulse_default_sink()?;
|
||||||
|
default_sink_name.push_str(".monitor");
|
||||||
|
|
||||||
|
let simple = Simple::new(
|
||||||
|
None,
|
||||||
|
"piano_thingy",
|
||||||
|
Direction::Record,
|
||||||
|
Some(&default_sink_name),
|
||||||
|
"sample_yoinker",
|
||||||
|
&spec,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(simple)
|
||||||
|
}
|
71
beat_detection/src/dsp.rs
Normal file
71
beat_detection/src/dsp.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Taken from Till's magic Arduino Sketch
|
||||||
|
*/
|
||||||
|
pub trait ZTransformFilter {
|
||||||
|
fn process(&mut self, sample: f32) -> f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20 - 200Hz Single Pole Bandpass IIR Filter
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BassFilter {
|
||||||
|
xv: [f32; 3],
|
||||||
|
yv: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZTransformFilter for BassFilter {
|
||||||
|
fn process(&mut self, sample: f32) -> f32 {
|
||||||
|
self.xv[0] = self.xv[1];
|
||||||
|
self.xv[1] = self.xv[2];
|
||||||
|
self.xv[2] = sample / 3.0f32;
|
||||||
|
|
||||||
|
self.yv[0] = self.yv[1];
|
||||||
|
self.yv[1] = self.yv[2];
|
||||||
|
self.yv[2] = (self.xv[2] - self.xv[0])
|
||||||
|
+ (-0.7960060012f32 * self.yv[0])
|
||||||
|
+ (1.7903124146f32 * self.yv[1]);
|
||||||
|
|
||||||
|
self.yv[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10Hz Single Pole Lowpass IIR Filter
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EnvelopeFilter {
|
||||||
|
xv: [f32; 2],
|
||||||
|
yv: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZTransformFilter for EnvelopeFilter {
|
||||||
|
fn process(&mut self, sample: f32) -> f32 {
|
||||||
|
self.xv[0] = self.xv[1];
|
||||||
|
self.xv[1] = sample / 50.0f32;
|
||||||
|
|
||||||
|
self.yv[0] = self.yv[1];
|
||||||
|
self.yv[1] = (self.xv[0] + self.xv[1]) + (0.9875119299f32 * self.yv[0]);
|
||||||
|
|
||||||
|
self.yv[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.7 - 3.0Hz Single Pole Bandpass IIR Filter
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BeatFilter {
|
||||||
|
xv: [f32; 3],
|
||||||
|
yv: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZTransformFilter for BeatFilter {
|
||||||
|
fn process(&mut self, sample: f32) -> f32 {
|
||||||
|
self.xv[0] = self.xv[1];
|
||||||
|
self.xv[1] = self.xv[2];
|
||||||
|
self.xv[2] = sample / 2.7f32;
|
||||||
|
|
||||||
|
self.yv[0] = self.yv[1];
|
||||||
|
self.yv[1] = self.yv[2];
|
||||||
|
self.yv[2] = (self.xv[2] - self.xv[0])
|
||||||
|
+ (-0.7169861741f32 * self.yv[0])
|
||||||
|
+ (1.4453653501f32 * self.yv[1]);
|
||||||
|
|
||||||
|
self.yv[2]
|
||||||
|
}
|
||||||
|
}
|
258
beat_detection/src/main.rs
Normal file
258
beat_detection/src/main.rs
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
mod capture;
|
||||||
|
mod dsp;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use pulse::sample::{Format, Spec};
|
||||||
|
use ringbuffer::{ConstGenericRingBuffer, RingBuffer, RingBufferExt, RingBufferWrite};
|
||||||
|
use sdl2::{
|
||||||
|
event::Event,
|
||||||
|
keyboard::Keycode,
|
||||||
|
pixels::Color,
|
||||||
|
rect::{Point, Rect},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::dsp::ZTransformFilter;
|
||||||
|
|
||||||
|
const SAMPLE_RATE: usize = 5000;
|
||||||
|
const FPS: usize = 50;
|
||||||
|
|
||||||
|
const BUFFER_SIZE: usize = SAMPLE_RATE / FPS;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
fn main() -> 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;
|
||||||
|
|
||||||
|
let mut threshold = 1.5f32;
|
||||||
|
|
||||||
|
let mut brightness = 0f32;
|
||||||
|
|
||||||
|
let mut beat_toggle = false;
|
||||||
|
|
||||||
|
// sdl
|
||||||
|
|
||||||
|
let sdl = sdl2::init().unwrap();
|
||||||
|
let sdl_video = sdl.video().unwrap();
|
||||||
|
|
||||||
|
let window = sdl_video
|
||||||
|
.window("Beat Detectinator", 800, 600)
|
||||||
|
.resizable()
|
||||||
|
.build()?;
|
||||||
|
let mut canvas = window.into_canvas().build()?;
|
||||||
|
let texture_creator = canvas.texture_creator();
|
||||||
|
let texture_size = 10 * POINT_COUNT as u32;
|
||||||
|
let mut texture = texture_creator.create_texture_target(
|
||||||
|
canvas.default_pixel_format(),
|
||||||
|
texture_size,
|
||||||
|
texture_size,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut point_buf = ConstGenericRingBuffer::<f32, POINT_BUFFER_SIZE>::new();
|
||||||
|
point_buf.fill_default();
|
||||||
|
|
||||||
|
let mut event_pump = sdl.event_pump().unwrap();
|
||||||
|
|
||||||
|
'running: loop {
|
||||||
|
for event in event_pump.poll_iter() {
|
||||||
|
match event {
|
||||||
|
Event::Quit { .. }
|
||||||
|
| Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Escape),
|
||||||
|
..
|
||||||
|
} => break 'running,
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Up),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
threshold += 0.01f32;
|
||||||
|
println!("threshold: {:.2}", threshold);
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Down),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
threshold -= 0.01f32;
|
||||||
|
println!("threshold: {:.2}", threshold);
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(k), ..
|
||||||
|
} => {
|
||||||
|
println!("{}", k)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
point_buf.push(beat);
|
||||||
|
|
||||||
|
canvas.with_texture_canvas(&mut texture, |canvas| {
|
||||||
|
let min_y = 0f32;
|
||||||
|
let max_y = 3f32;
|
||||||
|
|
||||||
|
let get_y = |y: &f32| {
|
||||||
|
|
||||||
|
let mut y = y.clone();
|
||||||
|
|
||||||
|
if y <= 0f32 {
|
||||||
|
y = 0f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
y = (1f32 + y).log2();
|
||||||
|
|
||||||
|
let mut y = (y - min_y) / (max_y - min_y);
|
||||||
|
|
||||||
|
((1f32 - y) * texture_size as f32) as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
// background
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = (255f32 * brightness) as u8;
|
||||||
|
|
||||||
|
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 = point_buf
|
||||||
|
.iter()
|
||||||
|
.skip(point_buf.capacity() - 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 = point_buf
|
||||||
|
.iter()
|
||||||
|
.skip(point_buf.capacity() - 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 point_buf
|
||||||
|
.iter()
|
||||||
|
.skip(point_buf.capacity() - POINT_COUNT)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let x = 10 * i;
|
||||||
|
let y = get_y(beat);
|
||||||
|
|
||||||
|
canvas
|
||||||
|
.draw_rect(Rect::new((x + 1) as i32, (y - 1) as i32, 8, 3))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
j = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 0, 0));
|
||||||
|
canvas.clear();
|
||||||
|
|
||||||
|
let (w, h) = canvas.window().drawable_size();
|
||||||
|
|
||||||
|
let target_rect = if w > h {
|
||||||
|
Rect::new(((w - h) / 2) as i32, 0, h, h)
|
||||||
|
} else {
|
||||||
|
Rect::new(0, ((h - w) / 2) as i32, w, w)
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.copy(&texture, None, Some(target_rect)).unwrap();
|
||||||
|
canvas.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user