|
|
|
|
@@ -26,7 +26,11 @@ impl Interpolation {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update interpolation with current state and return interpolated values
|
|
|
|
|
pub fn update(&mut self, state: &osc::State, current_time: Instant) -> Interpolated {
|
|
|
|
|
pub fn update<const COLS: usize, const ROWS: usize>(
|
|
|
|
|
&mut self,
|
|
|
|
|
state: &osc::State<COLS, ROWS>,
|
|
|
|
|
current_time: Instant,
|
|
|
|
|
) -> Interpolated {
|
|
|
|
|
// The OSC state is the source of truth for the base position and tempo.
|
|
|
|
|
let base_position = state.metronome_position;
|
|
|
|
|
let base_timestamp = state.metronome_timestamp;
|
|
|
|
|
@@ -80,22 +84,22 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_simple_forward_interpolation() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - should return the received position
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 100ms later with same OSC state (no new OSC update)
|
|
|
|
|
let elapsed_duration = Duration::from_millis(100);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate expected position
|
|
|
|
|
// At 120 BPM: beat_duration = 60.0 / 120.0 = 0.5 seconds
|
|
|
|
|
// elapsed = 0.1 seconds = 0.1 / 0.5 = 0.2 beat progress
|
|
|
|
|
@@ -109,31 +113,31 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_interpolation_with_incremented_time_no_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 100ms later with same OSC state (no new OSC update)
|
|
|
|
|
let elapsed_duration = Duration::from_millis(100);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
|
|
|
|
let expected_position = 0.4;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
assert!(!result2.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update with small time increment (1ms later) - should be nearly identical
|
|
|
|
|
let slightly_later_time = later_time + Duration::from_millis(1);
|
|
|
|
|
let result3 = interpolation.update(&state, slightly_later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Position should advance only slightly: 1ms / 500ms = 0.002
|
|
|
|
|
let expected_position_3 = 0.4 + 0.002;
|
|
|
|
|
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
|
|
|
|
@@ -144,27 +148,27 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_interpolation_with_identical_timestamps_no_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 100ms later with same OSC state (no new OSC update)
|
|
|
|
|
let elapsed_duration = Duration::from_millis(100);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
|
|
|
|
let expected_position = 0.4;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
assert!(!result2.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update with identical parameters as result2 - should be identical
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
assert!((result3.position_in_beat - result2.position_in_beat).abs() < 0.001);
|
|
|
|
|
@@ -175,17 +179,17 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_beat_boundary_detected_on_position_wraparound() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.8;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 150ms later - should cross beat boundary
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// Time to reach boundary from 0.8: (1.0 - 0.8) * 0.5 = 0.1 seconds = 100ms
|
|
|
|
|
@@ -194,7 +198,7 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
let elapsed_duration = Duration::from_millis(150);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
|
|
|
|
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
|
|
|
|
}
|
|
|
|
|
@@ -203,31 +207,31 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_interpolation_with_incremented_time_with_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.8;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 150ms later - should cross beat boundary
|
|
|
|
|
let elapsed_duration = Duration::from_millis(150);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
|
|
|
|
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update with small time increment (1ms later) - should NOT detect boundary again
|
|
|
|
|
let slightly_later_time = later_time + Duration::from_millis(1);
|
|
|
|
|
let result3 = interpolation.update(&state, slightly_later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(!result3.beat_boundary); // Boundary already detected, don't fire again
|
|
|
|
|
// Position should advance only slightly: 1ms / 500ms = 0.002
|
|
|
|
|
// Position should advance only slightly: 1ms / 500ms = 0.002
|
|
|
|
|
let expected_position_3 = 0.1 + 0.002;
|
|
|
|
|
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
|
|
|
|
}
|
|
|
|
|
@@ -236,30 +240,31 @@ mod interpolation_without_state_update_tests {
|
|
|
|
|
fn test_interpolation_with_identical_timestamps_with_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.8;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 150ms later - should cross beat boundary
|
|
|
|
|
let elapsed_duration = Duration::from_millis(150);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
|
|
|
|
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update with identical parameters - should NOT detect boundary again
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert!(!result3.beat_boundary); // Boundary already detected, don't fire again
|
|
|
|
|
assert!((result3.position_in_beat - result2.position_in_beat).abs() < 0.001); // Same position
|
|
|
|
|
assert!((result3.position_in_beat - result2.position_in_beat).abs() < 0.001);
|
|
|
|
|
// Same position
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -272,42 +277,42 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_matches_interpolated_position_exactly() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update 100ms later with same OSC state (interpolation only)
|
|
|
|
|
let elapsed_duration = Duration::from_millis(100);
|
|
|
|
|
let later_time = start_time + elapsed_duration;
|
|
|
|
|
let result2 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// elapsed = 0.1 seconds = 0.1 / 0.5 = 0.2 beat progress
|
|
|
|
|
// expected_position = 0.2 + 0.2 = 0.4
|
|
|
|
|
let expected_position = 0.4;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
assert!(!result2.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: OSC update arrives at same time with exact matching position
|
|
|
|
|
state.metronome_position = 0.4; // Matches our interpolated position
|
|
|
|
|
state.metronome_timestamp = later_time; // Update timestamp to current time
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should maintain the same position smoothly, no beat boundary
|
|
|
|
|
assert!((result3.position_in_beat - 0.4).abs() < 0.001);
|
|
|
|
|
assert!(!result3.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fourth update: Small time increment after OSC sync
|
|
|
|
|
let slightly_later_time = later_time + Duration::from_millis(10);
|
|
|
|
|
let result4 = interpolation.update(&state, slightly_later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating from the OSC-synchronized position
|
|
|
|
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
|
|
|
|
let expected_position_4 = 0.4 + 0.02;
|
|
|
|
|
@@ -319,41 +324,41 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_slightly_late_no_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.3;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.3).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We expect to interpolate for 100ms to reach position 0.5
|
|
|
|
|
// But OSC update arrives 20ms late (timestamp from 80ms ago) with position 0.46
|
|
|
|
|
let late_osc_time = start_time + Duration::from_millis(80);
|
|
|
|
|
let current_time = start_time + Duration::from_millis(100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// At 80ms: expected position = 0.3 + (0.08 / 0.5) = 0.3 + 0.16 = 0.46
|
|
|
|
|
// OSC reports 0.46 (matches expectation but arrives late)
|
|
|
|
|
state.metronome_position = 0.46;
|
|
|
|
|
state.metronome_timestamp = late_osc_time; // OSC timestamp is 80ms (20ms ago)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update called at 100ms with OSC data from 80ms
|
|
|
|
|
let result2 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should adjust to OSC truth plus interpolation for the 20ms difference
|
|
|
|
|
// From OSC position 0.46 at 80ms, advance 20ms: 0.46 + (0.02 / 0.5) = 0.46 + 0.04 = 0.50
|
|
|
|
|
let expected_position = 0.50;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Continue normally from corrected baseline
|
|
|
|
|
let later_time = current_time + Duration::from_millis(60);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating from the corrected position
|
|
|
|
|
// 60ms = 0.06 seconds = 0.06 / 0.5 = 0.12 beat progress
|
|
|
|
|
let expected_position_3 = 0.50 + 0.12; // = 0.62
|
|
|
|
|
@@ -365,39 +370,39 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_late_confirms_detected_boundary() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.9;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update: Interpolate past beat boundary (should detect it)
|
|
|
|
|
let boundary_cross_time = start_time + Duration::from_millis(80);
|
|
|
|
|
let result2 = interpolation.update(&state, boundary_cross_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// At 80ms: position = 0.9 + (0.08 / 0.5) = 0.9 + 0.16 = 1.06 → wraps to 0.06
|
|
|
|
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
|
|
|
|
assert!((result2.position_in_beat - 0.06).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Late OSC arrives confirming we're in next beat
|
|
|
|
|
let late_osc_time = start_time + Duration::from_millis(70); // 10ms ago
|
|
|
|
|
let current_time = start_time + Duration::from_millis(90);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OSC confirms we're in next beat with position 0.04 at 70ms
|
|
|
|
|
state.metronome_position = 0.04;
|
|
|
|
|
state.metronome_timestamp = late_osc_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let result3 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should NOT detect beat boundary again (already detected)
|
|
|
|
|
assert!(!result3.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should show current position: OSC 0.04 at 70ms + 20ms interpolation
|
|
|
|
|
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
|
|
|
|
let expected_position = 0.04 + 0.04; // = 0.08
|
|
|
|
|
@@ -408,39 +413,39 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_late_shows_missed_boundary() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.85;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update: Interpolate but don't cross boundary yet
|
|
|
|
|
let no_boundary_time = start_time + Duration::from_millis(60);
|
|
|
|
|
let result2 = interpolation.update(&state, no_boundary_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// At 60ms: position = 0.85 + (0.06 / 0.5) = 0.85 + 0.12 = 0.97
|
|
|
|
|
assert!(!result2.beat_boundary); // Should NOT detect boundary yet
|
|
|
|
|
assert!((result2.position_in_beat - 0.97).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Late OSC arrives showing we're actually in next beat
|
|
|
|
|
let late_osc_time = start_time + Duration::from_millis(70); // 20ms ago
|
|
|
|
|
let late_osc_time = start_time + Duration::from_millis(70); // 20ms ago
|
|
|
|
|
let current_time = start_time + Duration::from_millis(90);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OSC shows we're at position 0.1 in next beat, meaning boundary was crossed
|
|
|
|
|
state.metronome_position = 0.1;
|
|
|
|
|
state.metronome_timestamp = late_osc_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let result3 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should retroactively detect the missed beat boundary
|
|
|
|
|
assert!(result3.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should show current position: OSC 0.1 at 70ms + 20ms interpolation
|
|
|
|
|
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
|
|
|
|
let expected_position = 0.1 + 0.04; // = 0.14
|
|
|
|
|
@@ -451,44 +456,48 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_late_contradicts_detected_boundary() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.92;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.92).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update: Interpolate past beat boundary (should detect it)
|
|
|
|
|
let boundary_cross_time = start_time + Duration::from_millis(90);
|
|
|
|
|
let result2 = interpolation.update(&state, boundary_cross_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// At 90ms: position = 0.92 + (0.09 / 0.5) = 0.92 + 0.18 = 1.10 → wraps to 0.10
|
|
|
|
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
|
|
|
|
assert!((result2.position_in_beat - 0.10).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Late OSC arrives contradicting our boundary detection
|
|
|
|
|
let late_osc_time = start_time + Duration::from_millis(80); // 20ms ago
|
|
|
|
|
let current_time = start_time + Duration::from_millis(100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OSC shows we're still at position 0.98 in same beat (boundary not yet crossed)
|
|
|
|
|
state.metronome_position = 0.98;
|
|
|
|
|
state.metronome_timestamp = late_osc_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let result3 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should trust OSC data and NOT report beat boundary for this update
|
|
|
|
|
// (The previous boundary detection was premature/incorrect)
|
|
|
|
|
assert!(!result3.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should show current position: OSC 0.98 at 80ms + 20ms interpolation
|
|
|
|
|
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
|
|
|
|
let expected_position = 0.98 + 0.04; // = 1.02 → wraps to 0.02
|
|
|
|
|
let wrapped_position = if expected_position >= 1.0 { expected_position - 1.0 } else { expected_position };
|
|
|
|
|
let wrapped_position = if expected_position >= 1.0 {
|
|
|
|
|
expected_position - 1.0
|
|
|
|
|
} else {
|
|
|
|
|
expected_position
|
|
|
|
|
};
|
|
|
|
|
assert!((result3.position_in_beat - wrapped_position).abs() < 0.001);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -496,42 +505,42 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_very_late() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// OSC update arrives extremely late (600ms = more than one beat duration of 500ms)
|
|
|
|
|
let very_late_osc_time = start_time + Duration::from_millis(200); // OSC timestamp
|
|
|
|
|
let current_time = start_time + Duration::from_millis(800); // Current time (600ms later)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds = 500ms
|
|
|
|
|
// Gap of 600ms is more than one full beat, should trigger resync
|
|
|
|
|
state.metronome_position = 0.6;
|
|
|
|
|
state.metronome_timestamp = very_late_osc_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let result2 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// For very late updates, should do complete resync rather than huge interpolation
|
|
|
|
|
// Should base position on OSC data and the time difference, but handle gracefully
|
|
|
|
|
// 600ms gap = 600ms / 500ms = 1.2 beats = 1 full beat + 0.2 beat
|
|
|
|
|
// From OSC position 0.6, advance 0.2 beats: 0.6 + 0.2 = 0.8
|
|
|
|
|
let expected_position = 0.8;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.1); // Looser tolerance for edge case
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// May or may not detect beat boundary depending on implementation strategy
|
|
|
|
|
// The key is that it shouldn't crash or produce wildly incorrect results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Should continue normally after resync
|
|
|
|
|
let later_time = current_time + Duration::from_millis(50);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating smoothly from the resynced position
|
|
|
|
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
|
|
|
|
let expected_position_3 = result2.position_in_beat + 0.1;
|
|
|
|
|
@@ -547,41 +556,41 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_slightly_early_no_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.2;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We expect to interpolate for 100ms to reach position 0.4
|
|
|
|
|
// But OSC update arrives 10ms early (at 90ms) with position 0.36
|
|
|
|
|
let early_time = start_time + Duration::from_millis(90);
|
|
|
|
|
let current_time = start_time + Duration::from_millis(100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// At 90ms: expected position = 0.2 + (0.09 / 0.5) = 0.2 + 0.18 = 0.38
|
|
|
|
|
// But let's say OSC reports 0.36 (slightly behind due to processing delays)
|
|
|
|
|
state.metronome_position = 0.36;
|
|
|
|
|
state.metronome_timestamp = early_time; // OSC timestamp is 90ms
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update called at 100ms with OSC data from 90ms
|
|
|
|
|
let result2 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should adjust to OSC truth plus interpolation for the 10ms difference
|
|
|
|
|
// From OSC position 0.36 at 90ms, advance 10ms: 0.36 + (0.01 / 0.5) = 0.36 + 0.02 = 0.38
|
|
|
|
|
let expected_position = 0.38;
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Continue normally from corrected baseline
|
|
|
|
|
let later_time = current_time + Duration::from_millis(50);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating from the corrected position
|
|
|
|
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
|
|
|
|
let expected_position_3 = 0.38 + 0.1; // = 0.48
|
|
|
|
|
@@ -593,44 +602,44 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_slightly_early_with_past_beat() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.8;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We're going to interpolate for 80ms, which should put us at position 0.96
|
|
|
|
|
// But OSC update arrives showing we're actually at position 0.05 in the next beat
|
|
|
|
|
// This means the beat boundary was crossed somewhere in between
|
|
|
|
|
let update_time = start_time + Duration::from_millis(80);
|
|
|
|
|
let current_time = start_time + Duration::from_millis(90);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// Expected interpolated position at 80ms: 0.8 + (0.08 / 0.5) = 0.8 + 0.16 = 0.96
|
|
|
|
|
// But OSC shows position 0.05, meaning we crossed beat boundary and are 0.05 into next beat
|
|
|
|
|
state.metronome_position = 0.05;
|
|
|
|
|
state.metronome_timestamp = update_time; // OSC timestamp is 80ms
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update called at 90ms with OSC data from 80ms
|
|
|
|
|
let result2 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should detect that a beat boundary was crossed (based on OSC position being < previous)
|
|
|
|
|
assert!(result2.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should show current position: OSC position 0.05 at 80ms + 10ms interpolation
|
|
|
|
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
|
|
|
|
let expected_position = 0.05 + 0.02; // = 0.07
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Continue normally, should NOT detect beat boundary again
|
|
|
|
|
let later_time = current_time + Duration::from_millis(50);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating from the corrected position
|
|
|
|
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
|
|
|
|
let expected_position_3 = 0.07 + 0.1; // = 0.17
|
|
|
|
|
@@ -642,53 +651,53 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_arrives_slightly_early_with_beat_now() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.85;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We're going to interpolate for 60ms, which should put us at position 0.97
|
|
|
|
|
// But OSC update arrives 10ms early showing position 0.0 (beat boundary at OSC time)
|
|
|
|
|
let osc_time = start_time + Duration::from_millis(60);
|
|
|
|
|
let current_time = start_time + Duration::from_millis(70);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// Expected interpolated position at 60ms: 0.85 + (0.06 / 0.5) = 0.85 + 0.12 = 0.97
|
|
|
|
|
// But OSC shows position 0.0, meaning beat boundary occurred at the OSC timestamp
|
|
|
|
|
state.metronome_position = 0.0;
|
|
|
|
|
state.metronome_timestamp = osc_time; // OSC timestamp is 60ms (10ms ago)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update called at 70ms with OSC data from 60ms
|
|
|
|
|
let result2 = interpolation.update(&state, current_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should detect beat boundary (OSC shows we hit 0.0 at its timestamp)
|
|
|
|
|
assert!(result2.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should show current position: OSC position 0.0 at 60ms + 10ms interpolation
|
|
|
|
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
|
|
|
|
let expected_position = 0.0 + 0.02; // = 0.02
|
|
|
|
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Continue normally, should NOT detect beat boundary again
|
|
|
|
|
let later_time = current_time + Duration::from_millis(40);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue interpolating from the corrected position
|
|
|
|
|
// 40ms = 0.04 seconds = 0.04 / 0.5 = 0.08 beat progress
|
|
|
|
|
let expected_position_3 = 0.02 + 0.08; // = 0.10
|
|
|
|
|
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
|
|
|
|
assert!(!result3.beat_boundary); // No boundary on this update
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fourth update: Verify smooth continuation
|
|
|
|
|
let even_later_time = later_time + Duration::from_millis(25);
|
|
|
|
|
let result4 = interpolation.update(&state, even_later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 25ms = 0.025 seconds = 0.025 / 0.5 = 0.05 beat progress
|
|
|
|
|
let expected_position_4 = 0.10 + 0.05; // = 0.15
|
|
|
|
|
assert!((result4.position_in_beat - expected_position_4).abs() < 0.001);
|
|
|
|
|
@@ -699,47 +708,47 @@ mod state_update_tests {
|
|
|
|
|
fn test_osc_update_at_beat_boundary_exact_match() {
|
|
|
|
|
let start_time = Instant::now();
|
|
|
|
|
let mut interpolation = Interpolation::new(start_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create initial state with 120 BPM, starting near end of beat
|
|
|
|
|
let mut state = osc::State::new(5, 5, 120.0);
|
|
|
|
|
let mut state = osc::State::<5, 5>::new(120.0);
|
|
|
|
|
state.metronome_position = 0.9;
|
|
|
|
|
state.metronome_timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First update at start time - establish baseline
|
|
|
|
|
let result1 = interpolation.update(&state, start_time);
|
|
|
|
|
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
|
|
|
|
assert!(!result1.beat_boundary);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate when boundary should occur
|
|
|
|
|
// At 120 BPM: beat_duration = 0.5 seconds
|
|
|
|
|
// Time to reach boundary from 0.9: (1.0 - 0.9) * 0.5 = 0.05 seconds = 50ms
|
|
|
|
|
let boundary_time = start_time + Duration::from_millis(50);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second update: OSC update arrives exactly at calculated boundary time
|
|
|
|
|
state.metronome_position = 0.0; // Confirms we've crossed to new beat
|
|
|
|
|
state.metronome_timestamp = boundary_time;
|
|
|
|
|
let result2 = interpolation.update(&state, boundary_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should detect beat boundary and show position 0.0
|
|
|
|
|
assert!(result2.beat_boundary);
|
|
|
|
|
assert!((result2.position_in_beat - 0.0).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Third update: Continue 50ms later (should be at position 0.1 in new beat)
|
|
|
|
|
let later_time = boundary_time + Duration::from_millis(50);
|
|
|
|
|
let result3 = interpolation.update(&state, later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should NOT detect beat boundary again, and should be at position 0.1
|
|
|
|
|
assert!(!result3.beat_boundary);
|
|
|
|
|
let expected_position = 0.1; // 50ms / 500ms = 0.1
|
|
|
|
|
assert!((result3.position_in_beat - expected_position).abs() < 0.001);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fourth update: Small increment to verify smooth continuation
|
|
|
|
|
let slightly_later_time = later_time + Duration::from_millis(10);
|
|
|
|
|
let result4 = interpolation.update(&state, slightly_later_time);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Should continue smoothly without boundary detection
|
|
|
|
|
assert!(!result4.beat_boundary);
|
|
|
|
|
let expected_position_4 = 0.1 + 0.02; // 10ms = 0.02 beat progress
|
|
|
|
|
assert!((result4.position_in_beat - expected_position_4).abs() < 0.001);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|