Feat: really good lookahead mechanism for scheduling
This commit is contained in:
@@ -1,14 +1,6 @@
|
||||
/// Microsecond-precision timestamp for audio synchronization.
|
||||
pub type SyncTime = u64;
|
||||
|
||||
/// Convert beat duration to microseconds at given tempo.
|
||||
fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
|
||||
if tempo <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
((beats / tempo) * 60_000_000.0).round() as SyncTime
|
||||
}
|
||||
|
||||
/// Timing boundary types for step and pattern scheduling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepTiming {
|
||||
@@ -33,33 +25,55 @@ impl StepTiming {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate how many substeps were crossed between two beat positions.
|
||||
/// Speed multiplier affects the substep rate (2x speed = 2x substeps per beat).
|
||||
pub fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
|
||||
if prev_beat < 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
|
||||
(curr_substep - prev_substep).clamp(0, 16) as usize
|
||||
}
|
||||
|
||||
/// Calculate microseconds until the next substep boundary.
|
||||
pub fn micros_until_next_substep(current_beat: f64, speed: f64, tempo: f64) -> SyncTime {
|
||||
if tempo <= 0.0 || speed <= 0.0 {
|
||||
return 0;
|
||||
/// Return the beat positions of all substeps in the window [frontier, end).
|
||||
/// Each entry is the exact beat at which that substep fires.
|
||||
/// Clamped to 64 results max to prevent runaway.
|
||||
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
|
||||
if frontier < 0.0 || end <= frontier || speed <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let substeps_per_beat = 4.0 * speed;
|
||||
let current_substep = (current_beat * substeps_per_beat).floor();
|
||||
let next_substep_beat = (current_substep + 1.0) / substeps_per_beat;
|
||||
let beats_until = next_substep_beat - current_beat;
|
||||
beats_to_micros(beats_until, tempo)
|
||||
let first = (frontier * substeps_per_beat).floor() as i64 + 1;
|
||||
let last = (end * substeps_per_beat).floor() as i64;
|
||||
let count = (last - first + 1).clamp(0, 64) as usize;
|
||||
let mut result = Vec::with_capacity(count);
|
||||
for i in 0..count as i64 {
|
||||
result.push((first + i) as f64 / substeps_per_beat);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
|
||||
if tempo <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
((beats / tempo) * 60_000_000.0).round() as SyncTime
|
||||
}
|
||||
|
||||
fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
|
||||
if prev_beat < 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
|
||||
(curr_substep - prev_substep).clamp(0, 16) as usize
|
||||
}
|
||||
|
||||
fn micros_until_next_substep(current_beat: f64, speed: f64, tempo: f64) -> SyncTime {
|
||||
if tempo <= 0.0 || speed <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let substeps_per_beat = 4.0 * speed;
|
||||
let current_substep = (current_beat * substeps_per_beat).floor();
|
||||
let next_substep_beat = (current_substep + 1.0) / substeps_per_beat;
|
||||
let beats_until = next_substep_beat - current_beat;
|
||||
beats_to_micros(beats_until, tempo)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_beats_to_micros_at_120_bpm() {
|
||||
// At 120 BPM, one beat = 0.5 seconds = 500,000 microseconds
|
||||
@@ -159,4 +173,40 @@ mod tests {
|
||||
fn test_micros_until_next_substep_zero_speed() {
|
||||
assert_eq!(micros_until_next_substep(0.0, 0.0, 120.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_basic() {
|
||||
// At 1x speed, substeps at 0.25, 0.5, 0.75, 1.0...
|
||||
// Window [0.0, 0.5) should contain 0.25 and 0.5
|
||||
let result = substeps_in_window(0.0, 0.5, 1.0);
|
||||
assert_eq!(result, vec![0.25, 0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_2x_speed() {
|
||||
// At 2x speed, substeps at 0.125, 0.25, 0.375, 0.5...
|
||||
// Window [0.0, 0.5) should contain 4 substeps
|
||||
let result = substeps_in_window(0.0, 0.5, 2.0);
|
||||
assert_eq!(result, vec![0.125, 0.25, 0.375, 0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_mid_beat() {
|
||||
// Window [0.3, 0.6): should contain 0.5
|
||||
let result = substeps_in_window(0.3, 0.6, 1.0);
|
||||
assert_eq!(result, vec![0.5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_empty() {
|
||||
// Window too small to contain any substep
|
||||
let result = substeps_in_window(0.1, 0.2, 1.0);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_negative_frontier() {
|
||||
let result = substeps_in_window(-1.0, 0.5, 1.0);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user