WIP: not sure
This commit is contained in:
225
src/engine/timing.rs
Normal file
225
src/engine/timing.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Microsecond-precision timestamp for audio synchronization.
|
||||
pub type SyncTime = u64;
|
||||
|
||||
/// Sentinel value representing "never" or "no scheduled time".
|
||||
pub const NEVER: SyncTime = SyncTime::MAX;
|
||||
|
||||
/// Convert beat duration to microseconds at given tempo.
|
||||
pub fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
|
||||
if tempo <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
((beats / tempo) * 60_000_000.0).round() as SyncTime
|
||||
}
|
||||
|
||||
/// Convert microseconds to beats at given tempo.
|
||||
pub fn micros_to_beats(micros: SyncTime, tempo: f64) -> f64 {
|
||||
if tempo <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
(tempo * (micros as f64)) / 60_000_000.0
|
||||
}
|
||||
|
||||
/// Timing boundary types for step and pattern scheduling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepTiming {
|
||||
/// Fire at a specific absolute substep number.
|
||||
Substep(u64),
|
||||
/// Fire when any substep boundary is crossed (4 substeps per beat).
|
||||
NextSubstep,
|
||||
/// Fire when a beat boundary is crossed.
|
||||
NextBeat,
|
||||
/// Fire when a bar/quantum boundary is crossed.
|
||||
NextBar,
|
||||
}
|
||||
|
||||
impl StepTiming {
|
||||
/// Returns true if the boundary was crossed between prev_beat and curr_beat.
|
||||
pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool {
|
||||
if prev_beat < 0.0 {
|
||||
return false;
|
||||
}
|
||||
match self {
|
||||
Self::NextSubstep => {
|
||||
(prev_beat * 4.0).floor() as i64 != (curr_beat * 4.0).floor() as i64
|
||||
}
|
||||
Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64,
|
||||
Self::NextBar => {
|
||||
(prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64
|
||||
}
|
||||
Self::Substep(target) => {
|
||||
let prev_substep = (prev_beat * 4.0).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0).floor() as i64;
|
||||
prev_substep < *target as i64 && curr_substep >= *target as i64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Threshold for switching from sleep to active wait (100μs).
|
||||
pub const ACTIVE_WAIT_THRESHOLD_US: SyncTime = 100;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_beats_to_micros_at_120_bpm() {
|
||||
// At 120 BPM, one beat = 0.5 seconds = 500,000 microseconds
|
||||
assert_eq!(beats_to_micros(1.0, 120.0), 500_000);
|
||||
assert_eq!(beats_to_micros(2.0, 120.0), 1_000_000);
|
||||
assert_eq!(beats_to_micros(0.5, 120.0), 250_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_to_beats_at_120_bpm() {
|
||||
// At 120 BPM, 500,000 microseconds = 1 beat
|
||||
assert!((micros_to_beats(500_000, 120.0) - 1.0).abs() < 1e-10);
|
||||
assert!((micros_to_beats(1_000_000, 120.0) - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_tempo() {
|
||||
assert_eq!(beats_to_micros(1.0, 0.0), 0);
|
||||
assert_eq!(micros_to_beats(1_000_000, 0.0), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let tempo = 135.0;
|
||||
let beats = 3.75;
|
||||
let micros = beats_to_micros(beats, tempo);
|
||||
let back = micros_to_beats(micros, tempo);
|
||||
assert!((back - beats).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_substep_crossed() {
|
||||
// Crossing from substep 0 to substep 1 (beat 0.0 to 0.26)
|
||||
assert!(StepTiming::NextSubstep.crossed(0.0, 0.26, 4.0));
|
||||
// Not crossing (both in same substep)
|
||||
assert!(!StepTiming::NextSubstep.crossed(0.26, 0.27, 4.0));
|
||||
// Negative prev_beat returns false
|
||||
assert!(!StepTiming::NextSubstep.crossed(-1.0, 0.5, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_beat_crossed() {
|
||||
// Crossing from beat 0 to beat 1
|
||||
assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0));
|
||||
// Not crossing (both in same beat)
|
||||
assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0));
|
||||
// Negative prev_beat returns false
|
||||
assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_bar_crossed() {
|
||||
// Crossing from bar 0 to bar 1 (quantum=4)
|
||||
assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0));
|
||||
// Not crossing (both in same bar)
|
||||
assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0));
|
||||
// Crossing with different quantum
|
||||
assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_at_substep() {
|
||||
// Crossing to substep 4 (beat 1.0)
|
||||
assert!(StepTiming::Substep(4).crossed(0.9, 1.1, 4.0));
|
||||
// Not yet at substep 4
|
||||
assert!(!StepTiming::Substep(4).crossed(0.5, 0.9, 4.0));
|
||||
// Already past substep 4
|
||||
assert!(!StepTiming::Substep(4).crossed(1.5, 2.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_normal() {
|
||||
// One substep crossed at 1x speed
|
||||
assert_eq!(substeps_crossed(0.0, 0.26, 1.0), 1);
|
||||
// Two substeps crossed
|
||||
assert_eq!(substeps_crossed(0.0, 0.51, 1.0), 2);
|
||||
// No substep crossed
|
||||
assert_eq!(substeps_crossed(0.1, 0.2, 1.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_with_speed() {
|
||||
// At 2x speed, 0.5 beats = 4 substeps
|
||||
assert_eq!(substeps_crossed(0.0, 0.5, 2.0), 4);
|
||||
// At 0.5x speed, 0.5 beats = 1 substep
|
||||
assert_eq!(substeps_crossed(0.0, 0.5, 0.5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_negative_prev() {
|
||||
// Negative prev_beat returns 0
|
||||
assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_clamp() {
|
||||
// Large jump clamped to 16
|
||||
assert_eq!(substeps_crossed(0.0, 100.0, 1.0), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_until_next_substep_at_beat_zero() {
|
||||
// At beat 0.0, speed 1.0, tempo 120 BPM
|
||||
// Next substep is at beat 0.25 (1/4 beat)
|
||||
// 1/4 beat at 120 BPM = 0.25 / 120 * 60_000_000 = 125_000 μs
|
||||
let micros = micros_until_next_substep(0.0, 1.0, 120.0);
|
||||
assert_eq!(micros, 125_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_until_next_substep_near_boundary() {
|
||||
// At beat 0.24, almost at the substep boundary (0.25)
|
||||
// Next substep at 0.25, so 0.01 beats away
|
||||
let micros = micros_until_next_substep(0.24, 1.0, 120.0);
|
||||
// 0.01 beats at 120 BPM = 5000 μs
|
||||
assert_eq!(micros, 5000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_until_next_substep_with_speed() {
|
||||
// At 2x speed, substeps are at 0.125, 0.25, 0.375...
|
||||
// At beat 0.0, next substep is at 0.125
|
||||
let micros = micros_until_next_substep(0.0, 2.0, 120.0);
|
||||
// 0.125 beats at 120 BPM = 62_500 μs
|
||||
assert_eq!(micros, 62_500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_until_next_substep_zero_tempo() {
|
||||
assert_eq!(micros_until_next_substep(0.0, 1.0, 0.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micros_until_next_substep_zero_speed() {
|
||||
assert_eq!(micros_until_next_substep(0.0, 0.0, 120.0), 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user