/// 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 { /// 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 } /// 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_zero_tempo() { assert_eq!(beats_to_micros(1.0, 0.0), 0); } #[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); } }