Improve Beat Detection
This commit is contained in:
parent
25f5229afe
commit
bf62ceb98f
@ -3,30 +3,15 @@ import { Pattern, PatternOutput, Time } from './proto';
|
|||||||
|
|
||||||
export class ChaserPattern implements Pattern {
|
export class ChaserPattern implements Pattern {
|
||||||
|
|
||||||
lastBeat: number;
|
|
||||||
lastTime: number;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.lastBeat = 0;
|
|
||||||
this.lastTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(time: Time): PatternOutput {
|
render(time: Time): PatternOutput {
|
||||||
|
|
||||||
if (time.beatRelative === null) {
|
if (time.beatRelative === null) {
|
||||||
this.lastBeat = 0;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = time.beatRelative;
|
let t = time.beatRelative;
|
||||||
|
|
||||||
if (t < this.lastTime) {
|
let head_number = Math.floor(t % 4);
|
||||||
this.lastBeat += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTime = t;
|
|
||||||
|
|
||||||
let head_number = this.lastBeat % 4;
|
|
||||||
|
|
||||||
let template: MovingHeadState = {
|
let template: MovingHeadState = {
|
||||||
startAddress: 0,
|
startAddress: 0,
|
||||||
@ -44,7 +29,7 @@ export class ChaserPattern implements Pattern {
|
|||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
for (let [i, startAddress] of [1, 15, 29, 43].entries()) {
|
for (let [i, startAddress] of [1, 15, 29, 43].entries()) {
|
||||||
result[i] = { ...template};
|
result[i] = { ...template };
|
||||||
result[i].startAddress = startAddress;
|
result[i].startAddress = startAddress;
|
||||||
|
|
||||||
if (i === head_number) {
|
if (i === head_number) {
|
||||||
|
@ -20,7 +20,7 @@ const BUFFER_SIZE: usize = SAMPLE_RATE / PULSE_UPDATES_PER_SECOND;
|
|||||||
const POINTS_PER_SECOND: usize = SAMPLE_RATE / 200;
|
const POINTS_PER_SECOND: usize = SAMPLE_RATE / 200;
|
||||||
pub const MILLIS_PER_POINT: usize = 1000 / POINTS_PER_SECOND;
|
pub const MILLIS_PER_POINT: usize = 1000 / POINTS_PER_SECOND;
|
||||||
const POINT_MIN_COUNT: usize = 2 * POINTS_PER_SECOND;
|
const POINT_MIN_COUNT: usize = 2 * POINTS_PER_SECOND;
|
||||||
const POINT_BUFFER_SIZE: usize = POINT_MIN_COUNT.next_power_of_two();
|
pub const POINT_BUFFER_SIZE: usize = POINT_MIN_COUNT.next_power_of_two();
|
||||||
|
|
||||||
pub struct AudioCaptureThread {
|
pub struct AudioCaptureThread {
|
||||||
join_handle: Option<JoinHandle<Result<()>>>,
|
join_handle: Option<JoinHandle<Result<()>>>,
|
||||||
|
@ -105,7 +105,7 @@ pub fn get_progress(mut cx: FunctionContext) -> JsResult<JsObject> {
|
|||||||
let obj = cx.empty_object();
|
let obj = cx.empty_object();
|
||||||
|
|
||||||
let type_string;
|
let type_string;
|
||||||
match boxed_tracker.borrow().current_beat_progress() {
|
match boxed_tracker.borrow_mut().current_beat_progress() {
|
||||||
Some(progress) => {
|
Some(progress) => {
|
||||||
type_string = cx.string("some".to_string());
|
type_string = cx.string("some".to_string());
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ use num::Complex;
|
|||||||
use rustfft::FftPlanner;
|
use rustfft::FftPlanner;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::beat_tracking::audio::POINT_BUFFER_SIZE;
|
||||||
|
|
||||||
use super::audio::{AudioCaptureThread, MILLIS_PER_POINT};
|
use super::audio::{AudioCaptureThread, MILLIS_PER_POINT};
|
||||||
use super::metronome::Metronome;
|
use super::metronome::Metronome;
|
||||||
|
|
||||||
@ -22,6 +24,9 @@ pub struct BeatTracker {
|
|||||||
metronome: Metronome,
|
metronome: Metronome,
|
||||||
pub config: TrackerConfig,
|
pub config: TrackerConfig,
|
||||||
audio_capture_thread: AudioCaptureThread,
|
audio_capture_thread: AudioCaptureThread,
|
||||||
|
|
||||||
|
current_beats: Option<(Instant, Instant)>,
|
||||||
|
beat_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeatTracker {
|
impl BeatTracker {
|
||||||
@ -34,6 +39,9 @@ impl BeatTracker {
|
|||||||
zero_crossing_beat_delay: 0,
|
zero_crossing_beat_delay: 0,
|
||||||
},
|
},
|
||||||
audio_capture_thread: AudioCaptureThread::new(),
|
audio_capture_thread: AudioCaptureThread::new(),
|
||||||
|
|
||||||
|
current_beats: None,
|
||||||
|
beat_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,80 +109,116 @@ impl BeatTracker {
|
|||||||
(points, autocorrelation)
|
(points, autocorrelation)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_beat_progress(&self) -> Option<f64> {
|
// (period_length [ms], crossing [point index, timing relative to last_update])
|
||||||
|
fn get_correlation_data(&self) -> Option<(usize, usize)> {
|
||||||
|
let (points, autocorrelation) = self.get_graph_points();
|
||||||
|
|
||||||
|
if autocorrelation
|
||||||
|
.iter()
|
||||||
|
.reduce(|a, b| if a > b { a } else { b })
|
||||||
|
.unwrap()
|
||||||
|
< &self.config.ac_threshold
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut j = 0;
|
||||||
|
let i = loop {
|
||||||
|
j += 1;
|
||||||
|
|
||||||
|
if j == autocorrelation.len() {
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if autocorrelation[j - 1] > autocorrelation[j] {
|
||||||
|
break Some(j);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if i.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let period_length = i.unwrap();
|
||||||
|
|
||||||
|
// find zero crossing
|
||||||
|
|
||||||
|
let mut crossing = None;
|
||||||
|
|
||||||
|
for i in 1..points.len() {
|
||||||
|
if points[i - 1].signum() < 0.0 && points[i].signum() > 0.0 {
|
||||||
|
crossing = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if crossing.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let crossing = crossing.unwrap();
|
||||||
|
|
||||||
|
Some((period_length, crossing))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_beat_progress(&mut self) -> Option<f64> {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
match self.config.mode {
|
match self.config.mode {
|
||||||
TrackerMode::AUTO => {
|
TrackerMode::AUTO => {
|
||||||
let (points, autocorrelation) = self.get_graph_points();
|
// println!("reee");
|
||||||
|
|
||||||
if autocorrelation
|
let should_update = {
|
||||||
.iter()
|
match self.current_beats {
|
||||||
.reduce(|a, b| if a > b { a } else { b })
|
None => true,
|
||||||
.unwrap()
|
Some((_, next)) if now > next => {
|
||||||
< &self.config.ac_threshold
|
self.beat_count += 1;
|
||||||
{
|
true
|
||||||
return None;
|
}
|
||||||
}
|
_ => false,
|
||||||
|
|
||||||
let mut j = 0;
|
|
||||||
let i = loop {
|
|
||||||
j += 1;
|
|
||||||
|
|
||||||
if j == autocorrelation.len() {
|
|
||||||
break None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if autocorrelation[j - 1] > autocorrelation[j] {
|
|
||||||
break Some(j);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if i.is_none() {
|
if should_update {
|
||||||
return None;
|
if let Some((period_length, crossing)) = self.get_correlation_data() {
|
||||||
}
|
let last_update_timestamp = self.audio_capture_thread.get_last_update();
|
||||||
|
|
||||||
let period_length = i.unwrap();
|
let mut dt = (now - last_update_timestamp).as_millis() as i64;
|
||||||
|
dt += ((POINT_BUFFER_SIZE - crossing) * MILLIS_PER_POINT) as i64;
|
||||||
|
|
||||||
// println!("predicted period length: {}", period_length);
|
let mut prev = now - Duration::from_millis(dt as u64);
|
||||||
|
|
||||||
// find zero crossing
|
let period_millis = (period_length * MILLIS_PER_POINT) as u64;
|
||||||
|
|
||||||
|
let period = Duration::from_millis(period_millis);
|
||||||
|
|
||||||
let mut crossing = None;
|
let mut next = prev + period;
|
||||||
|
|
||||||
|
if let Some((_, old_next)) = self.current_beats {
|
||||||
|
while next < old_next + Duration::from_millis(period_millis / 2) {
|
||||||
|
prev += period;
|
||||||
|
next += period;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i in 1..points.len() {
|
self.current_beats = Some((prev, next));
|
||||||
if points[i - 1].signum() < 0.0 && points[i].signum() > 0.0 {
|
} else {
|
||||||
crossing = Some(i);
|
self.beat_count = 0;
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if crossing.is_none() {
|
let (prev, next) = self.current_beats.unwrap();
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let crossing = crossing.unwrap();
|
let fractional = if prev < now {
|
||||||
// println!("index of last positive zero crossing: {}", crossing);
|
(now - prev).as_millis() as f64 / (next - prev).as_millis() as f64
|
||||||
|
} else {
|
||||||
|
-1.0 * (prev - now).as_millis() as f64 / (next - prev).as_millis() as f64
|
||||||
|
};
|
||||||
|
|
||||||
let last_update_timestamp = self.audio_capture_thread.get_last_update();
|
Some(self.beat_count as f64 + fractional)
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
let mut dt = (now - last_update_timestamp).as_millis() as i64;
|
|
||||||
dt += ((points.len() - crossing) * MILLIS_PER_POINT) as i64;
|
|
||||||
dt += self.config.zero_crossing_beat_delay;
|
|
||||||
|
|
||||||
let dt = dt as f64;
|
|
||||||
|
|
||||||
let period_millis = (period_length * MILLIS_PER_POINT) as f64;
|
|
||||||
|
|
||||||
let relative_time = (dt % period_millis) / period_millis;
|
|
||||||
|
|
||||||
println!("relative time: {}", relative_time);
|
|
||||||
|
|
||||||
Some(relative_time)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackerMode::MANUAL => {
|
TrackerMode::MANUAL => self.metronome.get_beat_progress(now),
|
||||||
let now = Instant::now();
|
|
||||||
self.metronome.get_beat_progress(now)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user