diff --git a/audio_engine/src/main.rs b/audio_engine/src/main.rs index 7aad8bd..3419609 100644 --- a/audio_engine/src/main.rs +++ b/audio_engine/src/main.rs @@ -51,6 +51,9 @@ use track::Track; use track::TrackState; use track_matrix::TrackMatrix; +const COLS: usize = 5; +const ROWS: usize = 5; + pub struct JackPorts { pub audio_in: jack::Port, pub audio_out: jack::Port, @@ -66,10 +69,6 @@ async fn main() { .init() .expect("Could not initialize logger"); - let (mut osc, osc_controller) = Osc::new(&args.socket) - .await - .expect("Could not create OSC server"); - let (jack_client, ports) = setup_jack(); let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3); @@ -84,11 +83,19 @@ async fn main() { PersistenceManager::new(&args, notification_handler.subscribe()); let state = persistence_manager.state(); + // Calculate tempo from state and jack client + let tempo_bpm = (jack_client.sample_rate() as f32 * 60.0) + / state.borrow().metronome.frames_per_beat as f32; + + let (mut osc, osc_controller) = Osc::new(&args.socket, COLS, ROWS, tempo_bpm) + .await + .expect("Could not create OSC server"); + // Create post-record handler and get controller for ProcessHandler let (mut post_record_handler, post_record_controller) = PostRecordHandler::new(&args).expect("Could not create post-record handler"); - let process_handler = ProcessHandler::new( + let process_handler = ProcessHandler::<_, COLS, ROWS>::new( &jack_client, ports, allocator, @@ -183,4 +190,4 @@ fn setup_jack() -> (jack::Client, JackPorts) { }; (jack_client, ports) -} +} \ No newline at end of file diff --git a/audio_engine/src/midi.rs b/audio_engine/src/midi.rs index 384bbb5..a0d1c97 100644 --- a/audio_engine/src/midi.rs +++ b/audio_engine/src/midi.rs @@ -1,8 +1,8 @@ use crate::*; /// Process MIDI events -pub fn process_events( - process_handler: &mut ProcessHandler, +pub fn process_events( + process_handler: &mut ProcessHandler, ps: &jack::ProcessScope, ) -> Result<()> { // First, collect all MIDI events into a fixed-size array diff --git a/audio_engine/src/osc_server/server.rs b/audio_engine/src/osc_server/server.rs index 1db6a52..53e84c7 100644 --- a/audio_engine/src/osc_server/server.rs +++ b/audio_engine/src/osc_server/server.rs @@ -11,8 +11,13 @@ pub struct Osc { } impl Osc { - /// Create new OSC server and controller with same interface as before - pub async fn new(socket_path: &str) -> Result<(Self, OscController)> { + /// Create new OSC server and controller with configurable matrix size and tempo + pub async fn new( + socket_path: &str, + columns: usize, + rows: usize, + tempo: f32, + ) -> Result<(Self, OscController)> { // Create platform listener (server) let listener = osc_server::platform::create_listener(socket_path).await?; @@ -27,7 +32,7 @@ impl Osc { receiver, listener, broadcaster, - shadow_state: osc::State::new(5, 5), // Default 5x5 matrix + shadow_state: osc::State::new(columns, rows, tempo), }; Ok((server, controller)) @@ -105,4 +110,4 @@ impl Osc { Ok(()) } -} +} \ No newline at end of file diff --git a/audio_engine/src/process_handler.rs b/audio_engine/src/process_handler.rs index 0ae82a0..f10fa7d 100644 --- a/audio_engine/src/process_handler.rs +++ b/audio_engine/src/process_handler.rs @@ -1,9 +1,6 @@ use crate::*; -const COLS: usize = 5; -const ROWS: usize = 5; - -pub struct ProcessHandler { +pub struct ProcessHandler { post_record_controller: PostRecordController, osc: OscController, persistence: PersistenceManagerController, @@ -16,7 +13,7 @@ pub struct ProcessHandler { sample_rate: usize, } -impl ProcessHandler { +impl ProcessHandler { pub fn new( client: &jack::Client, ports: JackPorts, @@ -153,7 +150,7 @@ impl ProcessHandler { } } -impl jack::ProcessHandler for ProcessHandler { +impl jack::ProcessHandler for ProcessHandler { fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { if let Err(e) = self.process_with_error_handling(ps) { log::error!("Error processing audio: {}", e); @@ -164,7 +161,7 @@ impl jack::ProcessHandler for ProcessHandler { } } -impl ProcessHandler { +impl ProcessHandler { fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> { // Process metronome and get beat timing information let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?; @@ -184,4 +181,4 @@ impl ProcessHandler { Ok(()) } -} +} \ No newline at end of file diff --git a/gui/src/interpolation.rs b/gui/src/interpolation.rs new file mode 100644 index 0000000..39ad6d6 --- /dev/null +++ b/gui/src/interpolation.rs @@ -0,0 +1,549 @@ +use std::time::Instant; + +/// Result of interpolation calculation for one frame +#[derive(Debug, Clone)] +pub struct Interpolated { + /// Position within current beat (0.0 = start of beat, 1.0 = end of beat) + pub position_in_beat: f32, + /// True if a beat boundary occurred since the last frame + pub beat_boundary: bool, +} + +/// Handles interpolation of time-based values between OSC updates +#[derive(Debug)] +pub struct Interpolation { + /// Last received metronome position from OSC + last_osc_position: f32, + /// When the last OSC position was received + last_osc_timestamp: Instant, + /// Current tempo in BPM + tempo: f32, + /// When we last calculated interpolated values + last_frame_timestamp: Instant, + /// The position we calculated in the last frame + last_frame_position: f32, + /// Total number of beat boundaries we've reported (to avoid double-counting) + beats_reported: u64, + /// Whether we've received any OSC data yet + initialized: bool, +} + +impl Interpolation { + pub fn new(current_time: Instant) -> Self { + Self { + last_osc_position: 0.0, + last_osc_timestamp: current_time, + tempo: 120.0, + last_frame_timestamp: current_time, + last_frame_position: 0.0, + beats_reported: 0, + initialized: false, + } + } + + /// Update interpolation with current state and return interpolated values + pub fn update(&mut self, state: &osc::State, current_time: Instant) -> Interpolated { + if !self.initialized { + // First call - just initialize + self.last_osc_position = state.metronome_position; + self.last_osc_timestamp = state.metronome_timestamp; + self.tempo = state.tempo; + self.last_frame_timestamp = current_time; + self.last_frame_position = state.metronome_position; + self.beats_reported = 0; + self.initialized = true; + + return Interpolated { + position_in_beat: state.metronome_position, + beat_boundary: false, + }; + } + + let beat_duration_secs = 60.0 / self.tempo; + + // Check if we have new OSC data + let has_new_osc_data = state.metronome_timestamp != self.last_osc_timestamp; + + let current_position; + let mut beat_boundary = false; + + if has_new_osc_data { + // Update tempo first in case it changed + self.tempo = state.tempo; + let beat_duration_secs = 60.0 / self.tempo; + + // Check for beat boundary in the OSC data itself (position wraparound) + if state.metronome_position < self.last_osc_position && self.last_osc_position > 0.7 { + beat_boundary = true; + } + + // Use OSC position as our new reference and interpolate forward to current frame time + let elapsed_since_osc = current_time.duration_since(state.metronome_timestamp); + let advancement = elapsed_since_osc.as_secs_f32() / beat_duration_secs; + current_position = state.metronome_position + advancement; + + // Update our reference point + self.last_osc_position = state.metronome_position; + self.last_osc_timestamp = state.metronome_timestamp; + } else { + // No new OSC data - interpolate from our last reference point + let elapsed_since_osc = current_time.duration_since(self.last_osc_timestamp); + let advancement = elapsed_since_osc.as_secs_f32() / beat_duration_secs; + current_position = self.last_osc_position + advancement; + } + + // Detect beat boundary by comparing with last frame position + if !beat_boundary { + // Check if we crossed an integer boundary (beat boundary) + let last_beat = self.last_frame_position.floor(); + let current_beat = current_position.floor(); + + if current_beat > last_beat { + beat_boundary = true; + } + } + + // Normalize position to [0.0, 1.0) within current beat + let position_in_beat = current_position.fract(); + + // Update tracking for next frame + self.last_frame_timestamp = current_time; + self.last_frame_position = current_position; + + if beat_boundary { + self.beats_reported += 1; + } + + Interpolated { + position_in_beat, + beat_boundary, + } + } +} + +impl Default for Interpolation { + fn default() -> Self { + Self::new(Instant::now()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use osc::State; + use std::time::Duration; + + fn create_test_state(metronome_position: f32, tempo: f32, timestamp: Instant) -> State { + let mut state = State::new(5, 5, tempo); + state.set_metronome_position(metronome_position); + // Manually set the timestamp to make tests deterministic + state.metronome_timestamp = timestamp; + state + } + + #[test] + fn test_initial_state() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + let state = create_test_state(0.0, 120.0, start_time); + + let result = interpolation.update(&state, start_time); + + // First call should initialize but not report beat boundary + assert_eq!(result.position_in_beat, 0.0); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_normal_interpolation() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + let state = create_test_state(0.5, 120.0, start_time); + + // First update to initialize + interpolation.update(&state, start_time); + + // Simulate 100ms delay (at 120 BPM, one beat = 500ms, so this is 0.2 of a beat) + let later_time = start_time + Duration::from_millis(100); + + let result = interpolation.update(&state, later_time); + + // Position should be interpolated forward + assert!(result.position_in_beat > 0.5); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_beat_boundary_detection() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Start at position 0.9 + let state1 = create_test_state(0.9, 120.0, start_time); + interpolation.update(&state1, start_time); + + // New OSC update shows we're at beginning of next beat + let later_time = start_time + Duration::from_millis(100); + let state2 = create_test_state(0.1, 120.0, later_time); + let result = interpolation.update(&state2, later_time); + + assert_eq!(result.beat_boundary, true); + assert_eq!(result.position_in_beat, 0.1); + } + + #[test] + fn test_single_missed_osc_update() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize at 0.1 + let state1 = create_test_state(0.1, 120.0, start_time); + interpolation.update(&state1, start_time); + + // Frame update with no new OSC data - should interpolate + let frame_time = start_time + Duration::from_millis(50); + let result = interpolation.update(&state1, frame_time); + + // Should interpolate forward from 0.1 + assert!(result.position_in_beat > 0.1); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_single_late_osc_update() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize + let state1 = create_test_state(0.1, 120.0, start_time); + interpolation.update(&state1, start_time); + + // Late OSC update arrives after we've been interpolating + let osc_time = start_time + Duration::from_millis(100); + let frame_time = start_time + Duration::from_millis(150); + let state2 = create_test_state(0.3, 120.0, osc_time); + + let result = interpolation.update(&state2, frame_time); + + // Should sync to OSC data and continue interpolating + assert!(result.position_in_beat >= 0.3); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_multiple_missed_updates() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize + let state1 = create_test_state(0.8, 120.0, start_time); + interpolation.update(&state1, start_time); + + // Multiple frame updates without new OSC data, crossing beat boundary + let frame1_time = start_time + Duration::from_millis(100); + let result1 = interpolation.update(&state1, frame1_time); + // At 120 BPM: 100ms = 0.2 beats, so 0.8 + 0.2 = 1.0 (beat boundary) + assert_eq!(result1.beat_boundary, true); + + let frame2_time = start_time + Duration::from_millis(200); + let result2 = interpolation.update(&state1, frame2_time); + // No new beat boundary since we already crossed at 100ms + assert_eq!(result2.beat_boundary, false); + + let frame3_time = start_time + Duration::from_millis(600); + let result3 = interpolation.update(&state1, frame3_time); + // Another beat boundary should be detected (at 500ms mark) + assert_eq!(result3.beat_boundary, true); + } + + #[test] + fn test_full_missed_beat_several_updates() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize at 0.9 + let state1 = create_test_state(0.9, 120.0, start_time); + interpolation.update(&state1, start_time); + + // OSC update arrives showing we've crossed a full beat + let osc_time = start_time + Duration::from_millis(600); // > 1 beat at 120 BPM + let frame_time = start_time + Duration::from_millis(650); + let state2 = create_test_state(0.1, 120.0, osc_time); + + let result = interpolation.update(&state2, frame_time); + + // Should only report one beat boundary, not multiple + assert_eq!(result.beat_boundary, true); + // Position should be interpolated forward from OSC time to frame time + // 50ms at 120 BPM = 50/500 = 0.1 beat advancement + assert!((result.position_in_beat - 0.2).abs() < 0.01); // 0.1 + 0.1 = 0.2 + } + + #[test] + fn test_missing_updates_for_several_beats() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize with OSC data + let state1 = create_test_state(0.1, 120.0, start_time); + interpolation.update(&state1, start_time); + + let mut beat_count = 0; + + // Simulate regular frame updates (every 16ms) but OSC data becomes stale + // OSC data stops updating, but GUI keeps calling update() + for i in 1..=94 { + // 94 * 16ms = ~1500ms = 3 beats at 120 BPM + let frame_time = start_time + Duration::from_millis(i * 16); + + // Keep using the same stale OSC data (same timestamp) + let result = interpolation.update(&state1, frame_time); + + if result.beat_boundary { + beat_count += 1; + } + } + + // Should detect beat boundaries through time interpolation even with stale OSC data + // Starting at 0.1, over 1500ms at 120 BPM, should cross 3 beat boundaries + // (at ~450ms, ~950ms, ~1450ms from 0.1 starting point) + assert_eq!(beat_count, 3); + + // Finally, new OSC data arrives (simulating network recovery) + let late_osc_time = start_time + Duration::from_millis(1500); + let frame_time = start_time + Duration::from_millis(1500); + let state2 = create_test_state(0.7, 120.0, late_osc_time); + let result = interpolation.update(&state2, frame_time); + + // Should not trigger additional beat boundary since we already tracked them + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_no_double_beat_counting() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize + let state1 = create_test_state(0.9, 120.0, start_time); + interpolation.update(&state1, start_time); + + // First frame after beat boundary + let later_time = start_time + Duration::from_millis(100); + let state2 = create_test_state(0.1, 120.0, later_time); + let result1 = interpolation.update(&state2, later_time); + assert_eq!(result1.beat_boundary, true); + + // Second frame with same OSC data (no new timestamp) + let frame_time = later_time + Duration::from_millis(16); + let result2 = interpolation.update(&state2, frame_time); + assert_eq!(result2.beat_boundary, false); // Should not double-count + + // Third frame with same OSC data + let frame_time2 = later_time + Duration::from_millis(32); + let result3 = interpolation.update(&state2, frame_time2); + assert_eq!(result3.beat_boundary, false); // Should still not double-count + } + + #[test] + fn test_beat_counting_across_multiple_beats() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize + let state1 = create_test_state(0.1, 120.0, start_time); + interpolation.update(&state1, start_time); + + let mut beat_count = 0; + + // Simulate several beats passing through time interpolation + // At 120 BPM: 250ms = 0.5 beats, 500ms = 1 beat + // Starting at 0.1, beat boundaries at: 500ms, 1000ms, 1500ms, 2000ms, 2500ms + for i in 1..=10 { + let frame_time = start_time + Duration::from_millis(i * 250); // Half beat intervals + let result = interpolation.update(&state1, frame_time); + if result.beat_boundary { + beat_count += 1; + } + } + + // Should detect exactly 5 beat boundaries (at 500ms, 1000ms, 1500ms, 2000ms, 2500ms) + assert_eq!(beat_count, 5); + } + + #[test] + fn test_tempo_change_during_interpolation() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Start with 120 BPM at position 0.5 + let state1 = create_test_state(0.5, 120.0, start_time); + interpolation.update(&state1, start_time); + + // 100ms later, tempo changes to 60 BPM but no position update + let frame_time = start_time + Duration::from_millis(100); + let state2 = create_test_state(0.5, 60.0, start_time); // Same OSC timestamp, new tempo + let result = interpolation.update(&state2, frame_time); + + // Should still advance based on old tempo until new OSC data arrives + assert!(result.position_in_beat > 0.5); + assert_eq!(result.beat_boundary, false); + + // Now new OSC data arrives with the new tempo + let osc_time = start_time + Duration::from_millis(150); + let frame_time2 = start_time + Duration::from_millis(200); + let state3 = create_test_state(0.7, 60.0, osc_time); + let result2 = interpolation.update(&state3, frame_time2); + + // Should interpolate with new tempo: 50ms at 60 BPM = 50/1000 = 0.05 beat + assert!((result2.position_in_beat - 0.75).abs() < 0.01); + } + + #[test] + fn test_osc_data_at_exact_beat_boundary() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize at 0.9 + let state1 = create_test_state(0.9, 120.0, start_time); + interpolation.update(&state1, start_time); + + // OSC data arrives showing exact beat boundary (position 0.0) + let later_time = start_time + Duration::from_millis(100); + let state2 = create_test_state(0.0, 120.0, later_time); + let result = interpolation.update(&state2, later_time); + + assert_eq!(result.beat_boundary, true); + assert_eq!(result.position_in_beat, 0.0); + } + + #[test] + fn test_large_position_jump_without_beat() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize at 0.2 + let state1 = create_test_state(0.2, 120.0, start_time); + interpolation.update(&state1, start_time); + + // Large jump within same beat (shouldn't trigger beat boundary) + let later_time = start_time + Duration::from_millis(50); + let state2 = create_test_state(0.8, 120.0, later_time); + let result = interpolation.update(&state2, later_time); + + assert_eq!(result.beat_boundary, false); + assert!((result.position_in_beat - 0.8).abs() < 0.1); + } + + #[test] + fn test_position_wraparound() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Position near end of beat + let state1 = create_test_state(0.95, 120.0, start_time); + interpolation.update(&state1, start_time); + + // Position wraps to beginning + let later_time = start_time + Duration::from_millis(50); + let state2 = create_test_state(0.05, 120.0, later_time); + let result = interpolation.update(&state2, later_time); + + assert_eq!(result.beat_boundary, true); + assert_eq!(result.position_in_beat, 0.05); + } + + #[test] + fn test_backward_time_jump() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize at some position + let state1 = create_test_state(0.7, 120.0, start_time); + interpolation.update(&state1, start_time); + + // OSC update shows earlier position (shouldn't happen normally) + let later_time = start_time + Duration::from_millis(100); + let state2 = create_test_state(0.3, 120.0, later_time); + let result = interpolation.update(&state2, later_time); + + // Should handle gracefully without reporting false beat boundary + assert_eq!(result.position_in_beat, 0.3); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_multiple_consecutive_updates() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Series of rapid updates + for i in 0..10 { + let position = (i as f32) * 0.1; + let time = start_time + Duration::from_millis(i * 50); + let state = create_test_state(position, 120.0, time); + let result = interpolation.update(&state, time); + + if i == 0 { + assert_eq!(result.beat_boundary, false); // First update + } + // Beat boundary should only occur when crossing 1.0 -> 0.0 + } + } + + #[test] + fn test_rapid_frame_updates_between_osc() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Initialize + let state = create_test_state(0.2, 120.0, start_time); + interpolation.update(&state, start_time); + + // Multiple rapid frame updates without new OSC data + for i in 1..5 { + let frame_time = start_time + Duration::from_millis(i * 16); // ~60fps + let result = interpolation.update(&state, frame_time); + + // Each frame should advance position slightly + assert!(result.position_in_beat >= 0.2); + // No beat boundaries unless we cross a full beat + } + } + + #[test] + fn test_very_slow_tempo() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Very slow tempo (30 BPM = 2 second beats) + let state = create_test_state(0.1, 30.0, start_time); + interpolation.update(&state, start_time); + + // Small time advancement + let later_time = start_time + Duration::from_millis(100); + let result = interpolation.update(&state, later_time); + + // Position should advance very slowly + assert!(result.position_in_beat > 0.1); + assert!(result.position_in_beat < 0.2); + assert_eq!(result.beat_boundary, false); + } + + #[test] + fn test_very_fast_tempo() { + let start_time = Instant::now(); + let mut interpolation = Interpolation::new(start_time); + + // Very fast tempo (240 BPM = 250ms beats) + let state = create_test_state(0.8, 240.0, start_time); + interpolation.update(&state, start_time); + + // Time advancement that crosses beat boundary + let later_time = start_time + Duration::from_millis(100); + let result = interpolation.update(&state, later_time); + + // Should detect beat boundary quickly + assert_eq!(result.beat_boundary, true); + } +} diff --git a/gui/src/main.rs b/gui/src/main.rs index fd750b1..2231940 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,4 +1,6 @@ +mod interpolation; mod osc_client; +mod pendulum; mod ui; use anyhow::anyhow; diff --git a/gui/src/osc_client.rs b/gui/src/osc_client.rs index 11e42af..650d2db 100644 --- a/gui/src/osc_client.rs +++ b/gui/src/osc_client.rs @@ -23,7 +23,7 @@ pub struct Client { impl Client { pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result { - let initial_state = osc::State::new(columns, rows); + let initial_state = osc::State::new(columns, rows, 120.0); // Test data let initial_state = { let mut initial_state = initial_state; diff --git a/gui/src/pendulum.rs b/gui/src/pendulum.rs new file mode 100644 index 0000000..4dd3921 --- /dev/null +++ b/gui/src/pendulum.rs @@ -0,0 +1,40 @@ +use crate::interpolation::Interpolated; + +/// Handles pendulum swing direction and position calculation +#[derive(Debug)] +pub struct Pendulum { + /// Current swing direction (true = left to right, false = right to left) + going_right: bool, +} + +impl Pendulum { + /// Create a new pendulum starting in the right direction + pub fn new() -> Self { + Self { going_right: true } + } + + /// Update pendulum with interpolated timing data and return current position + /// + /// Returns position on rail where 0.0 = left side, 1.0 = right side + pub fn update(&mut self, interpolated: &Interpolated) -> f32 { + // Change direction on beat boundary + if interpolated.beat_boundary { + self.going_right = !self.going_right; + } + + // Calculate position based on current direction + if self.going_right { + // Left to right: 0.0 -> 0.0, 1.0 -> 1.0 + interpolated.position_in_beat + } else { + // Right to left: 0.0 -> 1.0, 1.0 -> 0.0 + 1.0 - interpolated.position_in_beat + } + } +} + +impl Default for Pendulum { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/gui/src/ui/app.rs b/gui/src/ui/app.rs index e370f50..930e4fd 100644 --- a/gui/src/ui/app.rs +++ b/gui/src/ui/app.rs @@ -1,16 +1,26 @@ pub struct App { state_receiver: tokio::sync::watch::Receiver, + interpolation: crate::interpolation::Interpolation, + pendulum: crate::pendulum::Pendulum, } impl App { pub fn new(state_receiver: tokio::sync::watch::Receiver) -> Self { - Self { state_receiver } + let interpolation = crate::interpolation::Interpolation::new(std::time::Instant::now()); + let pendulum = crate::pendulum::Pendulum::new(); + Self { + state_receiver, + interpolation, + pendulum, + } } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let state = self.state_receiver.borrow_and_update().clone(); + let interpolated = self.interpolation.update(&state, std::time::Instant::now()); + let pendulum_position = self.pendulum.update(&interpolated); ctx.style_mut(|style| { style.visuals.panel_fill = egui::Color32::from_rgb(17, 17, 17); @@ -20,7 +30,7 @@ impl eframe::App for App { egui::CentralPanel::default().show(ctx, |ui| { ui.vertical(|ui| { - ui.add(super::Metronome::new(state.metronome_position)); + ui.add(super::Metronome::new(pendulum_position)); ui.add(super::Matrix::new( &state.cells, state.selected_column, diff --git a/osc/src/message.rs b/osc/src/message.rs index c985219..6b84b24 100644 --- a/osc/src/message.rs +++ b/osc/src/message.rs @@ -21,6 +21,9 @@ pub enum Message { MetronomePosition { position: f32, // 0.0 - 1.0 position within current beat }, + Tempo { + bpm: f32, + }, } impl Message { @@ -59,6 +62,9 @@ impl Message { Message::MetronomePosition { position } => { state.set_metronome_position(*position); } + Message::Tempo { bpm } => { + state.set_tempo(*bpm); + } } } @@ -117,6 +123,11 @@ impl Message { position: *position, })); } + } else if addr == "/looper/tempo" { + if let Some(rosc::OscType::Float(bpm)) = args.first() { + log::trace!("Tempo: bpm={bpm}"); + return Ok(Some(Message::Tempo { bpm: *bpm })); + } } // Unknown or unsupported message Ok(None) @@ -168,6 +179,14 @@ impl Message { args: vec![rosc::OscType::Float(position)], }) } + Message::Tempo { bpm } => { + let address = "/looper/tempo".to_string(); + + rosc::OscPacket::Message(rosc::OscMessage { + addr: address, + args: vec![rosc::OscType::Float(bpm)], + }) + } } } -} +} \ No newline at end of file diff --git a/osc/src/state.rs b/osc/src/state.rs index d3aa66a..b3a964e 100644 --- a/osc/src/state.rs +++ b/osc/src/state.rs @@ -23,10 +23,11 @@ pub struct State { pub rows: usize, pub metronome_position: f32, // 0.0 - 1.0 position within current beat pub metronome_timestamp: std::time::Instant, // When position was last updated + pub tempo: f32, // BPM } impl State { - pub fn new(columns: usize, rows: usize) -> Self { + pub fn new(columns: usize, rows: usize, tempo: f32) -> Self { let cells = (0..columns) .map(|_| { vec![ @@ -47,6 +48,7 @@ impl State { rows, metronome_position: 0.0, metronome_timestamp: std::time::Instant::now(), + tempo, } } @@ -79,12 +81,18 @@ impl State { Message::MetronomePosition { position } => { self.set_metronome_position(*position); } + Message::Tempo { bpm } => { + self.set_tempo(*bpm); + } } } pub fn create_state_dump(&self) -> Vec { let mut messages = Vec::new(); + // Send tempo + messages.push(Message::Tempo { bpm: self.tempo }); + // Send current selections messages.push(Message::SelectedColumnChanged { column: self.selected_column, @@ -140,4 +148,8 @@ impl State { self.metronome_position = position; self.metronome_timestamp = std::time::Instant::now(); } -} + + pub fn set_tempo(&mut self, bpm: f32) { + self.tempo = bpm; + } +} \ No newline at end of file