First attempt at interpolation

This commit is contained in:
Geens 2025-06-22 11:44:46 +02:00
parent 7e6a539296
commit b35574c364
11 changed files with 667 additions and 26 deletions

View File

@ -51,6 +51,9 @@ use track::Track;
use track::TrackState; use track::TrackState;
use track_matrix::TrackMatrix; use track_matrix::TrackMatrix;
const COLS: usize = 5;
const ROWS: usize = 5;
pub struct JackPorts { pub struct JackPorts {
pub audio_in: jack::Port<jack::AudioIn>, pub audio_in: jack::Port<jack::AudioIn>,
pub audio_out: jack::Port<jack::AudioOut>, pub audio_out: jack::Port<jack::AudioOut>,
@ -66,10 +69,6 @@ async fn main() {
.init() .init()
.expect("Could not initialize logger"); .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 (jack_client, ports) = setup_jack();
let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3); let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3);
@ -84,11 +83,19 @@ async fn main() {
PersistenceManager::new(&args, notification_handler.subscribe()); PersistenceManager::new(&args, notification_handler.subscribe());
let state = persistence_manager.state(); 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 // Create post-record handler and get controller for ProcessHandler
let (mut post_record_handler, post_record_controller) = let (mut post_record_handler, post_record_controller) =
PostRecordHandler::new(&args).expect("Could not create post-record handler"); PostRecordHandler::new(&args).expect("Could not create post-record handler");
let process_handler = ProcessHandler::new( let process_handler = ProcessHandler::<_, COLS, ROWS>::new(
&jack_client, &jack_client,
ports, ports,
allocator, allocator,

View File

@ -1,8 +1,8 @@
use crate::*; use crate::*;
/// Process MIDI events /// Process MIDI events
pub fn process_events<F: ChunkFactory>( pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
process_handler: &mut ProcessHandler<F>, process_handler: &mut ProcessHandler<F, COLS, ROWS>,
ps: &jack::ProcessScope, ps: &jack::ProcessScope,
) -> Result<()> { ) -> Result<()> {
// First, collect all MIDI events into a fixed-size array // First, collect all MIDI events into a fixed-size array

View File

@ -11,8 +11,13 @@ pub struct Osc {
} }
impl Osc { impl Osc {
/// Create new OSC server and controller with same interface as before /// Create new OSC server and controller with configurable matrix size and tempo
pub async fn new(socket_path: &str) -> Result<(Self, OscController)> { pub async fn new(
socket_path: &str,
columns: usize,
rows: usize,
tempo: f32,
) -> Result<(Self, OscController)> {
// Create platform listener (server) // Create platform listener (server)
let listener = osc_server::platform::create_listener(socket_path).await?; let listener = osc_server::platform::create_listener(socket_path).await?;
@ -27,7 +32,7 @@ impl Osc {
receiver, receiver,
listener, listener,
broadcaster, broadcaster,
shadow_state: osc::State::new(5, 5), // Default 5x5 matrix shadow_state: osc::State::new(columns, rows, tempo),
}; };
Ok((server, controller)) Ok((server, controller))

View File

@ -1,9 +1,6 @@
use crate::*; use crate::*;
const COLS: usize = 5; pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize> {
const ROWS: usize = 5;
pub struct ProcessHandler<F: ChunkFactory> {
post_record_controller: PostRecordController, post_record_controller: PostRecordController,
osc: OscController, osc: OscController,
persistence: PersistenceManagerController, persistence: PersistenceManagerController,
@ -16,7 +13,7 @@ pub struct ProcessHandler<F: ChunkFactory> {
sample_rate: usize, sample_rate: usize,
} }
impl<F: ChunkFactory> ProcessHandler<F> { impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
pub fn new( pub fn new(
client: &jack::Client, client: &jack::Client,
ports: JackPorts, ports: JackPorts,
@ -153,7 +150,7 @@ impl<F: ChunkFactory> ProcessHandler<F> {
} }
} }
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> { impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler for ProcessHandler<F, COLS, ROWS> {
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
if let Err(e) = self.process_with_error_handling(ps) { if let Err(e) = self.process_with_error_handling(ps) {
log::error!("Error processing audio: {}", e); log::error!("Error processing audio: {}", e);
@ -164,7 +161,7 @@ impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
} }
} }
impl<F: ChunkFactory> ProcessHandler<F> { impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> { fn process_with_error_handling(&mut self, ps: &jack::ProcessScope) -> Result<()> {
// Process metronome and get beat timing information // Process metronome and get beat timing information
let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?; let timing = self.metronome.process(ps, &mut self.ports, &self.osc)?;

549
gui/src/interpolation.rs Normal file
View File

@ -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);
}
}

View File

@ -1,4 +1,6 @@
mod interpolation;
mod osc_client; mod osc_client;
mod pendulum;
mod ui; mod ui;
use anyhow::anyhow; use anyhow::anyhow;

View File

@ -23,7 +23,7 @@ pub struct Client {
impl Client { impl Client {
pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result<Self> { pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result<Self> {
let initial_state = osc::State::new(columns, rows); let initial_state = osc::State::new(columns, rows, 120.0);
// Test data // Test data
let initial_state = { let initial_state = {
let mut initial_state = initial_state; let mut initial_state = initial_state;

40
gui/src/pendulum.rs Normal file
View File

@ -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()
}
}

View File

@ -1,16 +1,26 @@
pub struct App { pub struct App {
state_receiver: tokio::sync::watch::Receiver<osc::State>, state_receiver: tokio::sync::watch::Receiver<osc::State>,
interpolation: crate::interpolation::Interpolation,
pendulum: crate::pendulum::Pendulum,
} }
impl App { impl App {
pub fn new(state_receiver: tokio::sync::watch::Receiver<osc::State>) -> Self { pub fn new(state_receiver: tokio::sync::watch::Receiver<osc::State>) -> 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 { impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let state = self.state_receiver.borrow_and_update().clone(); 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| { ctx.style_mut(|style| {
style.visuals.panel_fill = egui::Color32::from_rgb(17, 17, 17); 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| { egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.add(super::Metronome::new(state.metronome_position)); ui.add(super::Metronome::new(pendulum_position));
ui.add(super::Matrix::new( ui.add(super::Matrix::new(
&state.cells, &state.cells,
state.selected_column, state.selected_column,

View File

@ -21,6 +21,9 @@ pub enum Message {
MetronomePosition { MetronomePosition {
position: f32, // 0.0 - 1.0 position within current beat position: f32, // 0.0 - 1.0 position within current beat
}, },
Tempo {
bpm: f32,
},
} }
impl Message { impl Message {
@ -59,6 +62,9 @@ impl Message {
Message::MetronomePosition { position } => { Message::MetronomePosition { position } => {
state.set_metronome_position(*position); state.set_metronome_position(*position);
} }
Message::Tempo { bpm } => {
state.set_tempo(*bpm);
}
} }
} }
@ -117,6 +123,11 @@ impl Message {
position: *position, 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 // Unknown or unsupported message
Ok(None) Ok(None)
@ -168,6 +179,14 @@ impl Message {
args: vec![rosc::OscType::Float(position)], 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)],
})
}
} }
} }
} }

View File

@ -23,10 +23,11 @@ pub struct State {
pub rows: usize, pub rows: usize,
pub metronome_position: f32, // 0.0 - 1.0 position within current beat pub metronome_position: f32, // 0.0 - 1.0 position within current beat
pub metronome_timestamp: std::time::Instant, // When position was last updated pub metronome_timestamp: std::time::Instant, // When position was last updated
pub tempo: f32, // BPM
} }
impl State { impl State {
pub fn new(columns: usize, rows: usize) -> Self { pub fn new(columns: usize, rows: usize, tempo: f32) -> Self {
let cells = (0..columns) let cells = (0..columns)
.map(|_| { .map(|_| {
vec![ vec![
@ -47,6 +48,7 @@ impl State {
rows, rows,
metronome_position: 0.0, metronome_position: 0.0,
metronome_timestamp: std::time::Instant::now(), metronome_timestamp: std::time::Instant::now(),
tempo,
} }
} }
@ -79,12 +81,18 @@ impl State {
Message::MetronomePosition { position } => { Message::MetronomePosition { position } => {
self.set_metronome_position(*position); self.set_metronome_position(*position);
} }
Message::Tempo { bpm } => {
self.set_tempo(*bpm);
}
} }
} }
pub fn create_state_dump(&self) -> Vec<Message> { pub fn create_state_dump(&self) -> Vec<Message> {
let mut messages = Vec::new(); let mut messages = Vec::new();
// Send tempo
messages.push(Message::Tempo { bpm: self.tempo });
// Send current selections // Send current selections
messages.push(Message::SelectedColumnChanged { messages.push(Message::SelectedColumnChanged {
column: self.selected_column, column: self.selected_column,
@ -140,4 +148,8 @@ impl State {
self.metronome_position = position; self.metronome_position = position;
self.metronome_timestamp = std::time::Instant::now(); self.metronome_timestamp = std::time::Instant::now();
} }
pub fn set_tempo(&mut self, bpm: f32) {
self.tempo = bpm;
}
} }