OSC messages for column beat tracking
This commit is contained in:
parent
cfa2f1709e
commit
b4f51548b3
@ -118,14 +118,17 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
chunk_factory: &mut impl ChunkFactory,
|
chunk_factory: &mut impl ChunkFactory,
|
||||||
controllers: &ColumnControllers,
|
controllers: &ColumnControllers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let len = self.len();
|
let old_len = self.len();
|
||||||
|
|
||||||
if self.idle() {
|
if self.idle() {
|
||||||
if let Some(beat_index) = timing.beat_in_buffer {
|
if let Some(beat_index) = timing.beat_in_buffer {
|
||||||
let idle_time = input_buffer.len() - beat_index as usize;
|
// When starting from idle, always start at beat 0 (beginning of first beat)
|
||||||
if len == 0 {
|
// regardless of where in the buffer the beat occurs
|
||||||
self.playback_position = self.frames_per_beat - idle_time;
|
if old_len == 0 {
|
||||||
|
self.playback_position = 0; // Start at beat 0 for first recording
|
||||||
} else {
|
} else {
|
||||||
self.playback_position = len - idle_time;
|
let idle_time = input_buffer.len() - beat_index as usize;
|
||||||
|
self.playback_position = old_len - idle_time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,13 +150,38 @@ impl<const ROWS: usize> Column<ROWS> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = self.len();
|
let new_len = self.len();
|
||||||
if len > 0 {
|
|
||||||
self.playback_position = (self.playback_position + input_buffer.len()) % self.len();
|
if old_len == 0 && new_len > 0 {
|
||||||
} else {
|
let beats = new_len / self.frames_per_beat;
|
||||||
self.playback_position =
|
controllers.send_column_beats_changed(beats)?;
|
||||||
(self.playback_position + input_buffer.len()) % self.frames_per_beat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update playback position
|
||||||
|
if new_len > 0 {
|
||||||
|
self.playback_position = (self.playback_position + input_buffer.len()) % new_len;
|
||||||
|
} else {
|
||||||
|
// During recording of first track, don't use modulo - let position grow
|
||||||
|
// so that beat calculation works correctly
|
||||||
|
self.playback_position += input_buffer.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send beat change message if a beat occurred in this buffer
|
||||||
|
if timing.beat_in_buffer.is_some() {
|
||||||
|
// Calculate which beat we're entering (after the beat that just occurred)
|
||||||
|
let current_beat = if new_len > 0 {
|
||||||
|
// When column has defined length, calculate beat within that loop
|
||||||
|
let column_beats = new_len / self.frames_per_beat;
|
||||||
|
(self.playback_position / self.frames_per_beat) % column_beats
|
||||||
|
} else {
|
||||||
|
// During recording of first track, calculate beat based on absolute position
|
||||||
|
// The beat that just occurred is the beat we're now in
|
||||||
|
self.playback_position / self.frames_per_beat
|
||||||
|
};
|
||||||
|
|
||||||
|
controllers.send_column_beat_changed(current_beat)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,18 @@ impl<'a> ColumnControllers<'a> {
|
|||||||
row,
|
row,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_column_beats_changed(&self, beats: usize) -> Result<()> {
|
||||||
|
self.osc.column_beats_changed(self.column, beats)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_column_beat_changed(&self, beat: usize) -> Result<()> {
|
||||||
|
self.osc.column_beat_changed(self.column, beat)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column(&self) -> usize {
|
||||||
|
self.column
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TrackControllers<'a> {
|
impl<'a> TrackControllers<'a> {
|
||||||
|
|||||||
@ -84,10 +84,10 @@ async fn main() {
|
|||||||
let state = persistence_manager.state();
|
let state = persistence_manager.state();
|
||||||
|
|
||||||
// Calculate tempo from state and jack client
|
// Calculate tempo from state and jack client
|
||||||
let tempo_bpm = (jack_client.sample_rate() as f32 * 60.0)
|
let tempo_bpm =
|
||||||
/ state.borrow().metronome.frames_per_beat as f32;
|
(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)
|
let (mut osc, osc_controller) = Osc::<COLS, ROWS>::new(&args.socket, tempo_bpm)
|
||||||
.await
|
.await
|
||||||
.expect("Could not create OSC server");
|
.expect("Could not create OSC server");
|
||||||
|
|
||||||
@ -190,4 +190,4 @@ fn setup_jack() -> (jack::Client, JackPorts) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(jack_client, ports)
|
(jack_client, ports)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,4 +62,22 @@ impl OscController {
|
|||||||
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn column_beats_changed(&self, column: usize, beats: usize) -> Result<()> {
|
||||||
|
let message = osc::Message::ColumnBeatsChanged { column, beats };
|
||||||
|
match self.sender.try_send(message) {
|
||||||
|
Ok(true) => Ok(()),
|
||||||
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column_beat_changed(&self, column: usize, beat: usize) -> Result<()> {
|
||||||
|
let message = osc::Message::ColumnBeatChanged { column, beat };
|
||||||
|
match self.sender.try_send(message) {
|
||||||
|
Ok(true) => Ok(()),
|
||||||
|
Ok(false) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel full
|
||||||
|
Err(_) => Err(LooperError::Osc(std::panic::Location::caller())), // Channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,16 @@ use futures::SinkExt;
|
|||||||
|
|
||||||
const CLIENT_BUFFER_SIZE: usize = 32;
|
const CLIENT_BUFFER_SIZE: usize = 32;
|
||||||
|
|
||||||
pub struct Osc {
|
pub struct Osc<const COLS: usize, const ROWS: usize> {
|
||||||
receiver: kanal::AsyncReceiver<osc::Message>,
|
receiver: kanal::AsyncReceiver<osc::Message>,
|
||||||
listener: osc_server::platform::PlatformListener,
|
listener: osc_server::platform::PlatformListener,
|
||||||
broadcaster: tokio::sync::broadcast::Sender<osc::Message>,
|
broadcaster: tokio::sync::broadcast::Sender<osc::Message>,
|
||||||
shadow_state: osc::State,
|
shadow_state: osc::State<COLS, ROWS>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Osc {
|
impl<const COLS: usize, const ROWS: usize> Osc<COLS, ROWS> {
|
||||||
/// Create new OSC server and controller with configurable matrix size and tempo
|
/// Create new OSC server and controller with configurable tempo
|
||||||
pub async fn new(
|
pub async fn new(socket_path: &str, tempo: f32) -> Result<(Self, OscController)> {
|
||||||
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?;
|
||||||
|
|
||||||
@ -32,7 +27,7 @@ impl Osc {
|
|||||||
receiver,
|
receiver,
|
||||||
listener,
|
listener,
|
||||||
broadcaster,
|
broadcaster,
|
||||||
shadow_state: osc::State::new(columns, rows, tempo),
|
shadow_state: osc::State::new(tempo),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((server, controller))
|
Ok((server, controller))
|
||||||
@ -110,4 +105,4 @@ impl Osc {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,7 +150,9 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler for ProcessHandler<F, COLS, ROWS> {
|
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);
|
||||||
@ -181,4 +183,4 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,11 @@ impl Interpolation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update interpolation with current state and return interpolated values
|
/// 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.
|
// The OSC state is the source of truth for the base position and tempo.
|
||||||
let base_position = state.metronome_position;
|
let base_position = state.metronome_position;
|
||||||
let base_timestamp = state.metronome_timestamp;
|
let base_timestamp = state.metronome_timestamp;
|
||||||
@ -80,22 +84,22 @@ mod interpolation_without_state_update_tests {
|
|||||||
fn test_simple_forward_interpolation() {
|
fn test_simple_forward_interpolation() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - should return the received position
|
// First update at start time - should return the received position
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 100ms later with same OSC state (no new OSC update)
|
// Second update 100ms later with same OSC state (no new OSC update)
|
||||||
let elapsed_duration = Duration::from_millis(100);
|
let elapsed_duration = Duration::from_millis(100);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Calculate expected position
|
// Calculate expected position
|
||||||
// At 120 BPM: beat_duration = 60.0 / 120.0 = 0.5 seconds
|
// At 120 BPM: beat_duration = 60.0 / 120.0 = 0.5 seconds
|
||||||
// elapsed = 0.1 seconds = 0.1 / 0.5 = 0.2 beat progress
|
// 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() {
|
fn test_interpolation_with_incremented_time_no_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time
|
// First update at start time
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 100ms later with same OSC state (no new OSC update)
|
// Second update 100ms later with same OSC state (no new OSC update)
|
||||||
let elapsed_duration = Duration::from_millis(100);
|
let elapsed_duration = Duration::from_millis(100);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
||||||
let expected_position = 0.4;
|
let expected_position = 0.4;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
assert!(!result2.beat_boundary);
|
assert!(!result2.beat_boundary);
|
||||||
|
|
||||||
// Third update with small time increment (1ms later) - should be nearly identical
|
// Third update with small time increment (1ms later) - should be nearly identical
|
||||||
let slightly_later_time = later_time + Duration::from_millis(1);
|
let slightly_later_time = later_time + Duration::from_millis(1);
|
||||||
let result3 = interpolation.update(&state, slightly_later_time);
|
let result3 = interpolation.update(&state, slightly_later_time);
|
||||||
|
|
||||||
// Position should advance only slightly: 1ms / 500ms = 0.002
|
// Position should advance only slightly: 1ms / 500ms = 0.002
|
||||||
let expected_position_3 = 0.4 + 0.002;
|
let expected_position_3 = 0.4 + 0.002;
|
||||||
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
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() {
|
fn test_interpolation_with_identical_timestamps_no_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time
|
// First update at start time
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 100ms later with same OSC state (no new OSC update)
|
// Second update 100ms later with same OSC state (no new OSC update)
|
||||||
let elapsed_duration = Duration::from_millis(100);
|
let elapsed_duration = Duration::from_millis(100);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
// Expected position: 0.2 + (0.1 / 0.5) = 0.4
|
||||||
let expected_position = 0.4;
|
let expected_position = 0.4;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
assert!(!result2.beat_boundary);
|
assert!(!result2.beat_boundary);
|
||||||
|
|
||||||
// Third update with identical parameters as result2 - should be identical
|
// Third update with identical parameters as result2 - should be identical
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
assert!((result3.position_in_beat - result2.position_in_beat).abs() < 0.001);
|
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() {
|
fn test_beat_boundary_detected_on_position_wraparound() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.8;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time
|
// First update at start time
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 150ms later - should cross beat boundary
|
// Second update 150ms later - should cross beat boundary
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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 elapsed_duration = Duration::from_millis(150);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
assert!(result2.beat_boundary); // Should detect boundary crossing
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
||||||
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
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() {
|
fn test_interpolation_with_incremented_time_with_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.8;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time
|
// First update at start time
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 150ms later - should cross beat boundary
|
// Second update 150ms later - should cross beat boundary
|
||||||
let elapsed_duration = Duration::from_millis(150);
|
let elapsed_duration = Duration::from_millis(150);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
assert!(result2.beat_boundary); // Should detect boundary crossing
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
||||||
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
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
|
// Third update with small time increment (1ms later) - should NOT detect boundary again
|
||||||
let slightly_later_time = later_time + Duration::from_millis(1);
|
let slightly_later_time = later_time + Duration::from_millis(1);
|
||||||
let result3 = interpolation.update(&state, slightly_later_time);
|
let result3 = interpolation.update(&state, slightly_later_time);
|
||||||
|
|
||||||
assert!(!result3.beat_boundary); // Boundary already detected, don't fire again
|
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;
|
let expected_position_3 = 0.1 + 0.002;
|
||||||
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
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() {
|
fn test_interpolation_with_identical_timestamps_with_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.8;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time
|
// First update at start time
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 150ms later - should cross beat boundary
|
// Second update 150ms later - should cross beat boundary
|
||||||
let elapsed_duration = Duration::from_millis(150);
|
let elapsed_duration = Duration::from_millis(150);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
assert!(result2.beat_boundary); // Should detect boundary crossing
|
assert!(result2.beat_boundary); // Should detect boundary crossing
|
||||||
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
assert!((result2.position_in_beat - 0.1).abs() < 0.001); // Wrapped around
|
||||||
|
|
||||||
// Third update with identical parameters - should NOT detect boundary again
|
// Third update with identical parameters - should NOT detect boundary again
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
assert!(!result3.beat_boundary); // Boundary already detected, don't fire again
|
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() {
|
fn test_osc_update_matches_interpolated_position_exactly() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update 100ms later with same OSC state (interpolation only)
|
// Second update 100ms later with same OSC state (interpolation only)
|
||||||
let elapsed_duration = Duration::from_millis(100);
|
let elapsed_duration = Duration::from_millis(100);
|
||||||
let later_time = start_time + elapsed_duration;
|
let later_time = start_time + elapsed_duration;
|
||||||
let result2 = interpolation.update(&state, later_time);
|
let result2 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// At 120 BPM: beat_duration = 0.5 seconds
|
||||||
// elapsed = 0.1 seconds = 0.1 / 0.5 = 0.2 beat progress
|
// elapsed = 0.1 seconds = 0.1 / 0.5 = 0.2 beat progress
|
||||||
// expected_position = 0.2 + 0.2 = 0.4
|
// expected_position = 0.2 + 0.2 = 0.4
|
||||||
let expected_position = 0.4;
|
let expected_position = 0.4;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
assert!(!result2.beat_boundary);
|
assert!(!result2.beat_boundary);
|
||||||
|
|
||||||
// Third update: OSC update arrives at same time with exact matching position
|
// Third update: OSC update arrives at same time with exact matching position
|
||||||
state.metronome_position = 0.4; // Matches our interpolated position
|
state.metronome_position = 0.4; // Matches our interpolated position
|
||||||
state.metronome_timestamp = later_time; // Update timestamp to current time
|
state.metronome_timestamp = later_time; // Update timestamp to current time
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should maintain the same position smoothly, no beat boundary
|
// Should maintain the same position smoothly, no beat boundary
|
||||||
assert!((result3.position_in_beat - 0.4).abs() < 0.001);
|
assert!((result3.position_in_beat - 0.4).abs() < 0.001);
|
||||||
assert!(!result3.beat_boundary);
|
assert!(!result3.beat_boundary);
|
||||||
|
|
||||||
// Fourth update: Small time increment after OSC sync
|
// Fourth update: Small time increment after OSC sync
|
||||||
let slightly_later_time = later_time + Duration::from_millis(10);
|
let slightly_later_time = later_time + Duration::from_millis(10);
|
||||||
let result4 = interpolation.update(&state, slightly_later_time);
|
let result4 = interpolation.update(&state, slightly_later_time);
|
||||||
|
|
||||||
// Should continue interpolating from the OSC-synchronized position
|
// Should continue interpolating from the OSC-synchronized position
|
||||||
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
||||||
let expected_position_4 = 0.4 + 0.02;
|
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() {
|
fn test_osc_update_arrives_slightly_late_no_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.3;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.3).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.3).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// We expect to interpolate for 100ms to reach position 0.5
|
// 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
|
// 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 late_osc_time = start_time + Duration::from_millis(80);
|
||||||
let current_time = start_time + Duration::from_millis(100);
|
let current_time = start_time + Duration::from_millis(100);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// At 120 BPM: beat_duration = 0.5 seconds
|
||||||
// At 80ms: expected position = 0.3 + (0.08 / 0.5) = 0.3 + 0.16 = 0.46
|
// 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)
|
// OSC reports 0.46 (matches expectation but arrives late)
|
||||||
state.metronome_position = 0.46;
|
state.metronome_position = 0.46;
|
||||||
state.metronome_timestamp = late_osc_time; // OSC timestamp is 80ms (20ms ago)
|
state.metronome_timestamp = late_osc_time; // OSC timestamp is 80ms (20ms ago)
|
||||||
|
|
||||||
// Update called at 100ms with OSC data from 80ms
|
// Update called at 100ms with OSC data from 80ms
|
||||||
let result2 = interpolation.update(&state, current_time);
|
let result2 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should adjust to OSC truth plus interpolation for the 20ms difference
|
// 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
|
// 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;
|
let expected_position = 0.50;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
||||||
|
|
||||||
// Third update: Continue normally from corrected baseline
|
// Third update: Continue normally from corrected baseline
|
||||||
let later_time = current_time + Duration::from_millis(60);
|
let later_time = current_time + Duration::from_millis(60);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should continue interpolating from the corrected position
|
// Should continue interpolating from the corrected position
|
||||||
// 60ms = 0.06 seconds = 0.06 / 0.5 = 0.12 beat progress
|
// 60ms = 0.06 seconds = 0.06 / 0.5 = 0.12 beat progress
|
||||||
let expected_position_3 = 0.50 + 0.12; // = 0.62
|
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() {
|
fn test_osc_update_arrives_late_confirms_detected_boundary() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.9;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update: Interpolate past beat boundary (should detect it)
|
// Second update: Interpolate past beat boundary (should detect it)
|
||||||
let boundary_cross_time = start_time + Duration::from_millis(80);
|
let boundary_cross_time = start_time + Duration::from_millis(80);
|
||||||
let result2 = interpolation.update(&state, boundary_cross_time);
|
let result2 = interpolation.update(&state, boundary_cross_time);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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.beat_boundary); // Should detect boundary crossing
|
||||||
assert!((result2.position_in_beat - 0.06).abs() < 0.001);
|
assert!((result2.position_in_beat - 0.06).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Late OSC arrives confirming we're in next beat
|
// Third update: Late OSC arrives confirming we're in next beat
|
||||||
let late_osc_time = start_time + Duration::from_millis(70); // 10ms ago
|
let late_osc_time = start_time + Duration::from_millis(70); // 10ms ago
|
||||||
let current_time = start_time + Duration::from_millis(90);
|
let current_time = start_time + Duration::from_millis(90);
|
||||||
|
|
||||||
// OSC confirms we're in next beat with position 0.04 at 70ms
|
// OSC confirms we're in next beat with position 0.04 at 70ms
|
||||||
state.metronome_position = 0.04;
|
state.metronome_position = 0.04;
|
||||||
state.metronome_timestamp = late_osc_time;
|
state.metronome_timestamp = late_osc_time;
|
||||||
|
|
||||||
let result3 = interpolation.update(&state, current_time);
|
let result3 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should NOT detect beat boundary again (already detected)
|
// Should NOT detect beat boundary again (already detected)
|
||||||
assert!(!result3.beat_boundary);
|
assert!(!result3.beat_boundary);
|
||||||
|
|
||||||
// Should show current position: OSC 0.04 at 70ms + 20ms interpolation
|
// Should show current position: OSC 0.04 at 70ms + 20ms interpolation
|
||||||
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
||||||
let expected_position = 0.04 + 0.04; // = 0.08
|
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() {
|
fn test_osc_update_arrives_late_shows_missed_boundary() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.85;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update: Interpolate but don't cross boundary yet
|
// Second update: Interpolate but don't cross boundary yet
|
||||||
let no_boundary_time = start_time + Duration::from_millis(60);
|
let no_boundary_time = start_time + Duration::from_millis(60);
|
||||||
let result2 = interpolation.update(&state, no_boundary_time);
|
let result2 = interpolation.update(&state, no_boundary_time);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// At 120 BPM: beat_duration = 0.5 seconds
|
||||||
// At 60ms: position = 0.85 + (0.06 / 0.5) = 0.85 + 0.12 = 0.97
|
// 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.beat_boundary); // Should NOT detect boundary yet
|
||||||
assert!((result2.position_in_beat - 0.97).abs() < 0.001);
|
assert!((result2.position_in_beat - 0.97).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Late OSC arrives showing we're actually in next beat
|
// 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);
|
let current_time = start_time + Duration::from_millis(90);
|
||||||
|
|
||||||
// OSC shows we're at position 0.1 in next beat, meaning boundary was crossed
|
// OSC shows we're at position 0.1 in next beat, meaning boundary was crossed
|
||||||
state.metronome_position = 0.1;
|
state.metronome_position = 0.1;
|
||||||
state.metronome_timestamp = late_osc_time;
|
state.metronome_timestamp = late_osc_time;
|
||||||
|
|
||||||
let result3 = interpolation.update(&state, current_time);
|
let result3 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should retroactively detect the missed beat boundary
|
// Should retroactively detect the missed beat boundary
|
||||||
assert!(result3.beat_boundary);
|
assert!(result3.beat_boundary);
|
||||||
|
|
||||||
// Should show current position: OSC 0.1 at 70ms + 20ms interpolation
|
// Should show current position: OSC 0.1 at 70ms + 20ms interpolation
|
||||||
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
||||||
let expected_position = 0.1 + 0.04; // = 0.14
|
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() {
|
fn test_osc_update_arrives_late_contradicts_detected_boundary() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.92;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.92).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.92).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Second update: Interpolate past beat boundary (should detect it)
|
// Second update: Interpolate past beat boundary (should detect it)
|
||||||
let boundary_cross_time = start_time + Duration::from_millis(90);
|
let boundary_cross_time = start_time + Duration::from_millis(90);
|
||||||
let result2 = interpolation.update(&state, boundary_cross_time);
|
let result2 = interpolation.update(&state, boundary_cross_time);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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.beat_boundary); // Should detect boundary crossing
|
||||||
assert!((result2.position_in_beat - 0.10).abs() < 0.001);
|
assert!((result2.position_in_beat - 0.10).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Late OSC arrives contradicting our boundary detection
|
// Third update: Late OSC arrives contradicting our boundary detection
|
||||||
let late_osc_time = start_time + Duration::from_millis(80); // 20ms ago
|
let late_osc_time = start_time + Duration::from_millis(80); // 20ms ago
|
||||||
let current_time = start_time + Duration::from_millis(100);
|
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)
|
// OSC shows we're still at position 0.98 in same beat (boundary not yet crossed)
|
||||||
state.metronome_position = 0.98;
|
state.metronome_position = 0.98;
|
||||||
state.metronome_timestamp = late_osc_time;
|
state.metronome_timestamp = late_osc_time;
|
||||||
|
|
||||||
let result3 = interpolation.update(&state, current_time);
|
let result3 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should trust OSC data and NOT report beat boundary for this update
|
// Should trust OSC data and NOT report beat boundary for this update
|
||||||
// (The previous boundary detection was premature/incorrect)
|
// (The previous boundary detection was premature/incorrect)
|
||||||
assert!(!result3.beat_boundary);
|
assert!(!result3.beat_boundary);
|
||||||
|
|
||||||
// Should show current position: OSC 0.98 at 80ms + 20ms interpolation
|
// Should show current position: OSC 0.98 at 80ms + 20ms interpolation
|
||||||
// 20ms = 0.02 seconds = 0.02 / 0.5 = 0.04 beat progress
|
// 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 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);
|
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() {
|
fn test_osc_update_arrives_very_late() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// OSC update arrives extremely late (600ms = more than one beat duration of 500ms)
|
// 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 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)
|
let current_time = start_time + Duration::from_millis(800); // Current time (600ms later)
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds = 500ms
|
// At 120 BPM: beat_duration = 0.5 seconds = 500ms
|
||||||
// Gap of 600ms is more than one full beat, should trigger resync
|
// Gap of 600ms is more than one full beat, should trigger resync
|
||||||
state.metronome_position = 0.6;
|
state.metronome_position = 0.6;
|
||||||
state.metronome_timestamp = very_late_osc_time;
|
state.metronome_timestamp = very_late_osc_time;
|
||||||
|
|
||||||
let result2 = interpolation.update(&state, current_time);
|
let result2 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// For very late updates, should do complete resync rather than huge interpolation
|
// 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
|
// 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
|
// 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
|
// From OSC position 0.6, advance 0.2 beats: 0.6 + 0.2 = 0.8
|
||||||
let expected_position = 0.8;
|
let expected_position = 0.8;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.1); // Looser tolerance for edge case
|
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
|
// May or may not detect beat boundary depending on implementation strategy
|
||||||
// The key is that it shouldn't crash or produce wildly incorrect results
|
// The key is that it shouldn't crash or produce wildly incorrect results
|
||||||
|
|
||||||
// Third update: Should continue normally after resync
|
// Third update: Should continue normally after resync
|
||||||
let later_time = current_time + Duration::from_millis(50);
|
let later_time = current_time + Duration::from_millis(50);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should continue interpolating smoothly from the resynced position
|
// Should continue interpolating smoothly from the resynced position
|
||||||
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
||||||
let expected_position_3 = result2.position_in_beat + 0.1;
|
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() {
|
fn test_osc_update_arrives_slightly_early_no_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM
|
// 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_position = 0.2;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.2).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// We expect to interpolate for 100ms to reach position 0.4
|
// We expect to interpolate for 100ms to reach position 0.4
|
||||||
// But OSC update arrives 10ms early (at 90ms) with position 0.36
|
// But OSC update arrives 10ms early (at 90ms) with position 0.36
|
||||||
let early_time = start_time + Duration::from_millis(90);
|
let early_time = start_time + Duration::from_millis(90);
|
||||||
let current_time = start_time + Duration::from_millis(100);
|
let current_time = start_time + Duration::from_millis(100);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// At 120 BPM: beat_duration = 0.5 seconds
|
||||||
// At 90ms: expected position = 0.2 + (0.09 / 0.5) = 0.2 + 0.18 = 0.38
|
// 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)
|
// But let's say OSC reports 0.36 (slightly behind due to processing delays)
|
||||||
state.metronome_position = 0.36;
|
state.metronome_position = 0.36;
|
||||||
state.metronome_timestamp = early_time; // OSC timestamp is 90ms
|
state.metronome_timestamp = early_time; // OSC timestamp is 90ms
|
||||||
|
|
||||||
// Update called at 100ms with OSC data from 90ms
|
// Update called at 100ms with OSC data from 90ms
|
||||||
let result2 = interpolation.update(&state, current_time);
|
let result2 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should adjust to OSC truth plus interpolation for the 10ms difference
|
// 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
|
// 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;
|
let expected_position = 0.38;
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
assert!(!result2.beat_boundary); // No beat boundary in this scenario
|
||||||
|
|
||||||
// Third update: Continue normally from corrected baseline
|
// Third update: Continue normally from corrected baseline
|
||||||
let later_time = current_time + Duration::from_millis(50);
|
let later_time = current_time + Duration::from_millis(50);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should continue interpolating from the corrected position
|
// Should continue interpolating from the corrected position
|
||||||
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
||||||
let expected_position_3 = 0.38 + 0.1; // = 0.48
|
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() {
|
fn test_osc_update_arrives_slightly_early_with_past_beat() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.8;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.8).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// We're going to interpolate for 80ms, which should put us at position 0.96
|
// 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
|
// 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
|
// This means the beat boundary was crossed somewhere in between
|
||||||
let update_time = start_time + Duration::from_millis(80);
|
let update_time = start_time + Duration::from_millis(80);
|
||||||
let current_time = start_time + Duration::from_millis(90);
|
let current_time = start_time + Duration::from_millis(90);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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
|
// 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_position = 0.05;
|
||||||
state.metronome_timestamp = update_time; // OSC timestamp is 80ms
|
state.metronome_timestamp = update_time; // OSC timestamp is 80ms
|
||||||
|
|
||||||
// Update called at 90ms with OSC data from 80ms
|
// Update called at 90ms with OSC data from 80ms
|
||||||
let result2 = interpolation.update(&state, current_time);
|
let result2 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should detect that a beat boundary was crossed (based on OSC position being < previous)
|
// Should detect that a beat boundary was crossed (based on OSC position being < previous)
|
||||||
assert!(result2.beat_boundary);
|
assert!(result2.beat_boundary);
|
||||||
|
|
||||||
// Should show current position: OSC position 0.05 at 80ms + 10ms interpolation
|
// Should show current position: OSC position 0.05 at 80ms + 10ms interpolation
|
||||||
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
||||||
let expected_position = 0.05 + 0.02; // = 0.07
|
let expected_position = 0.05 + 0.02; // = 0.07
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Continue normally, should NOT detect beat boundary again
|
// Third update: Continue normally, should NOT detect beat boundary again
|
||||||
let later_time = current_time + Duration::from_millis(50);
|
let later_time = current_time + Duration::from_millis(50);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should continue interpolating from the corrected position
|
// Should continue interpolating from the corrected position
|
||||||
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
// 50ms = 0.05 seconds = 0.05 / 0.5 = 0.1 beat progress
|
||||||
let expected_position_3 = 0.07 + 0.1; // = 0.17
|
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() {
|
fn test_osc_update_arrives_slightly_early_with_beat_now() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.85;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.85).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// We're going to interpolate for 60ms, which should put us at position 0.97
|
// 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)
|
// 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 osc_time = start_time + Duration::from_millis(60);
|
||||||
let current_time = start_time + Duration::from_millis(70);
|
let current_time = start_time + Duration::from_millis(70);
|
||||||
|
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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
|
// But OSC shows position 0.0, meaning beat boundary occurred at the OSC timestamp
|
||||||
state.metronome_position = 0.0;
|
state.metronome_position = 0.0;
|
||||||
state.metronome_timestamp = osc_time; // OSC timestamp is 60ms (10ms ago)
|
state.metronome_timestamp = osc_time; // OSC timestamp is 60ms (10ms ago)
|
||||||
|
|
||||||
// Update called at 70ms with OSC data from 60ms
|
// Update called at 70ms with OSC data from 60ms
|
||||||
let result2 = interpolation.update(&state, current_time);
|
let result2 = interpolation.update(&state, current_time);
|
||||||
|
|
||||||
// Should detect beat boundary (OSC shows we hit 0.0 at its timestamp)
|
// Should detect beat boundary (OSC shows we hit 0.0 at its timestamp)
|
||||||
assert!(result2.beat_boundary);
|
assert!(result2.beat_boundary);
|
||||||
|
|
||||||
// Should show current position: OSC position 0.0 at 60ms + 10ms interpolation
|
// Should show current position: OSC position 0.0 at 60ms + 10ms interpolation
|
||||||
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
// 10ms = 0.01 seconds = 0.01 / 0.5 = 0.02 beat progress
|
||||||
let expected_position = 0.0 + 0.02; // = 0.02
|
let expected_position = 0.0 + 0.02; // = 0.02
|
||||||
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result2.position_in_beat - expected_position).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Continue normally, should NOT detect beat boundary again
|
// Third update: Continue normally, should NOT detect beat boundary again
|
||||||
let later_time = current_time + Duration::from_millis(40);
|
let later_time = current_time + Duration::from_millis(40);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should continue interpolating from the corrected position
|
// Should continue interpolating from the corrected position
|
||||||
// 40ms = 0.04 seconds = 0.04 / 0.5 = 0.08 beat progress
|
// 40ms = 0.04 seconds = 0.04 / 0.5 = 0.08 beat progress
|
||||||
let expected_position_3 = 0.02 + 0.08; // = 0.10
|
let expected_position_3 = 0.02 + 0.08; // = 0.10
|
||||||
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
assert!((result3.position_in_beat - expected_position_3).abs() < 0.001);
|
||||||
assert!(!result3.beat_boundary); // No boundary on this update
|
assert!(!result3.beat_boundary); // No boundary on this update
|
||||||
|
|
||||||
// Fourth update: Verify smooth continuation
|
// Fourth update: Verify smooth continuation
|
||||||
let even_later_time = later_time + Duration::from_millis(25);
|
let even_later_time = later_time + Duration::from_millis(25);
|
||||||
let result4 = interpolation.update(&state, even_later_time);
|
let result4 = interpolation.update(&state, even_later_time);
|
||||||
|
|
||||||
// 25ms = 0.025 seconds = 0.025 / 0.5 = 0.05 beat progress
|
// 25ms = 0.025 seconds = 0.025 / 0.5 = 0.05 beat progress
|
||||||
let expected_position_4 = 0.10 + 0.05; // = 0.15
|
let expected_position_4 = 0.10 + 0.05; // = 0.15
|
||||||
assert!((result4.position_in_beat - expected_position_4).abs() < 0.001);
|
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() {
|
fn test_osc_update_at_beat_boundary_exact_match() {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let mut interpolation = Interpolation::new(start_time);
|
let mut interpolation = Interpolation::new(start_time);
|
||||||
|
|
||||||
// Create initial state with 120 BPM, starting near end of beat
|
// 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_position = 0.9;
|
||||||
state.metronome_timestamp = start_time;
|
state.metronome_timestamp = start_time;
|
||||||
|
|
||||||
// First update at start time - establish baseline
|
// First update at start time - establish baseline
|
||||||
let result1 = interpolation.update(&state, start_time);
|
let result1 = interpolation.update(&state, start_time);
|
||||||
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
assert!((result1.position_in_beat - 0.9).abs() < 0.001);
|
||||||
assert!(!result1.beat_boundary);
|
assert!(!result1.beat_boundary);
|
||||||
|
|
||||||
// Calculate when boundary should occur
|
// Calculate when boundary should occur
|
||||||
// At 120 BPM: beat_duration = 0.5 seconds
|
// 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
|
// 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);
|
let boundary_time = start_time + Duration::from_millis(50);
|
||||||
|
|
||||||
// Second update: OSC update arrives exactly at calculated boundary time
|
// Second update: OSC update arrives exactly at calculated boundary time
|
||||||
state.metronome_position = 0.0; // Confirms we've crossed to new beat
|
state.metronome_position = 0.0; // Confirms we've crossed to new beat
|
||||||
state.metronome_timestamp = boundary_time;
|
state.metronome_timestamp = boundary_time;
|
||||||
let result2 = interpolation.update(&state, boundary_time);
|
let result2 = interpolation.update(&state, boundary_time);
|
||||||
|
|
||||||
// Should detect beat boundary and show position 0.0
|
// Should detect beat boundary and show position 0.0
|
||||||
assert!(result2.beat_boundary);
|
assert!(result2.beat_boundary);
|
||||||
assert!((result2.position_in_beat - 0.0).abs() < 0.001);
|
assert!((result2.position_in_beat - 0.0).abs() < 0.001);
|
||||||
|
|
||||||
// Third update: Continue 50ms later (should be at position 0.1 in new beat)
|
// Third update: Continue 50ms later (should be at position 0.1 in new beat)
|
||||||
let later_time = boundary_time + Duration::from_millis(50);
|
let later_time = boundary_time + Duration::from_millis(50);
|
||||||
let result3 = interpolation.update(&state, later_time);
|
let result3 = interpolation.update(&state, later_time);
|
||||||
|
|
||||||
// Should NOT detect beat boundary again, and should be at position 0.1
|
// Should NOT detect beat boundary again, and should be at position 0.1
|
||||||
assert!(!result3.beat_boundary);
|
assert!(!result3.beat_boundary);
|
||||||
let expected_position = 0.1; // 50ms / 500ms = 0.1
|
let expected_position = 0.1; // 50ms / 500ms = 0.1
|
||||||
assert!((result3.position_in_beat - expected_position).abs() < 0.001);
|
assert!((result3.position_in_beat - expected_position).abs() < 0.001);
|
||||||
|
|
||||||
// Fourth update: Small increment to verify smooth continuation
|
// Fourth update: Small increment to verify smooth continuation
|
||||||
let slightly_later_time = later_time + Duration::from_millis(10);
|
let slightly_later_time = later_time + Duration::from_millis(10);
|
||||||
let result4 = interpolation.update(&state, slightly_later_time);
|
let result4 = interpolation.update(&state, slightly_later_time);
|
||||||
|
|
||||||
// Should continue smoothly without boundary detection
|
// Should continue smoothly without boundary detection
|
||||||
assert!(!result4.beat_boundary);
|
assert!(!result4.beat_boundary);
|
||||||
let expected_position_4 = 0.1 + 0.02; // 10ms = 0.02 beat progress
|
let expected_position_4 = 0.1 + 0.02; // 10ms = 0.02 beat progress
|
||||||
assert!((result4.position_in_beat - expected_position_4).abs() < 0.001);
|
assert!((result4.position_in_beat - expected_position_4).abs() < 0.001);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,9 @@ mod ui;
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
const COLUMNS: usize = 5;
|
||||||
|
const ROWS: usize = 5;
|
||||||
|
|
||||||
trait DisplayStr {
|
trait DisplayStr {
|
||||||
fn display_str(&self) -> &'static str;
|
fn display_str(&self) -> &'static str;
|
||||||
}
|
}
|
||||||
@ -25,18 +28,17 @@ impl DisplayStr for osc::TrackState {
|
|||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Configuration
|
// Configuration
|
||||||
let socket_path = "fcb_looper.sock";
|
let socket_path = "fcb_looper.sock";
|
||||||
let columns = 5;
|
|
||||||
let rows = 5;
|
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
simple_logger::SimpleLogger::new()
|
simple_logger::SimpleLogger::new()
|
||||||
.with_level(log::LevelFilter::Info)
|
.with_level(log::LevelFilter::Info)
|
||||||
.with_module_level("gui::osc_client", log::LevelFilter::Off)
|
.with_module_level("gui::osc_client", log::LevelFilter::Off)
|
||||||
|
.with_module_level("osc", log::LevelFilter::Debug)
|
||||||
.init()
|
.init()
|
||||||
.expect("Could not initialize logger");
|
.expect("Could not initialize logger");
|
||||||
|
|
||||||
// Create OSC client
|
// Create OSC client
|
||||||
let osc_client = osc_client::Client::new(socket_path, columns, rows)?;
|
let osc_client = osc_client::Client::<COLUMNS, ROWS>::new(socket_path)?;
|
||||||
let state_receiver = osc_client.state_receiver();
|
let state_receiver = osc_client.state_receiver();
|
||||||
|
|
||||||
// Spawn OSC receiver thread on tokio runtime
|
// Spawn OSC receiver thread on tokio runtime
|
||||||
@ -58,7 +60,7 @@ async fn main() -> Result<()> {
|
|||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"FCB Looper",
|
"FCB Looper",
|
||||||
options,
|
options,
|
||||||
Box::new(|_cc| Ok(Box::new(ui::App::new(state_receiver)))),
|
Box::new(|_cc| Ok(Box::new(ui::App::<COLUMNS, ROWS>::new(state_receiver)))),
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to run egui: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to run egui: {}", e))?;
|
||||||
|
|
||||||
|
|||||||
@ -15,32 +15,33 @@ type PlatformStream = UnixStream;
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
type PlatformStream = NamedPipeClient;
|
type PlatformStream = NamedPipeClient;
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client<const COLS: usize, const ROWS: usize> {
|
||||||
socket_path: String,
|
socket_path: String,
|
||||||
state_sender: watch::Sender<osc::State>,
|
state_sender: watch::Sender<osc::State<COLS, ROWS>>,
|
||||||
state_receiver: watch::Receiver<osc::State>,
|
state_receiver: watch::Receiver<osc::State<COLS, ROWS>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl<const COLS: usize, const ROWS: usize> Client<COLS, ROWS> {
|
||||||
pub fn new(socket_path: &str, columns: usize, rows: usize) -> Result<Self> {
|
pub fn new(socket_path: &str) -> Result<Self> {
|
||||||
let initial_state = osc::State::new(columns, rows, 120.0);
|
let initial_state = osc::State::new(120.0);
|
||||||
// Test data
|
// Test data
|
||||||
let initial_state = {
|
let initial_state = {
|
||||||
let mut initial_state = initial_state;
|
let mut initial_state = initial_state;
|
||||||
initial_state.selected_column = 2;
|
initial_state.selected_column = if COLS > 2 { 2 } else { 0 };
|
||||||
initial_state.selected_row = 3;
|
initial_state.selected_row = if ROWS > 3 { 3 } else { 0 };
|
||||||
initial_state.cells[0][0].state = osc::TrackState::Playing;
|
|
||||||
initial_state.cells[0][0].volume = 0.3;
|
initial_state.columns[0].tracks[0].state = osc::TrackState::Playing;
|
||||||
initial_state.cells[0][1].state = osc::TrackState::Idle;
|
initial_state.columns[0].tracks[0].volume = 0.3;
|
||||||
initial_state.cells[0][1].volume = 1.0;
|
initial_state.columns[0].tracks[1].state = osc::TrackState::Idle;
|
||||||
initial_state.cells[1][0].state = osc::TrackState::Idle;
|
initial_state.columns[0].tracks[1].volume = 1.0;
|
||||||
initial_state.cells[1][0].volume = 0.9;
|
initial_state.columns[1].tracks[0].state = osc::TrackState::Idle;
|
||||||
initial_state.cells[1][1].state = osc::TrackState::Playing;
|
initial_state.columns[1].tracks[0].volume = 0.9;
|
||||||
initial_state.cells[1][1].volume = 0.8;
|
initial_state.columns[1].tracks[1].state = osc::TrackState::Playing;
|
||||||
initial_state.cells[2][0].state = osc::TrackState::Idle;
|
initial_state.columns[1].tracks[1].volume = 0.8;
|
||||||
initial_state.cells[2][0].volume = 0.7;
|
initial_state.columns[2].tracks[0].state = osc::TrackState::Idle;
|
||||||
initial_state.cells[2][3].state = osc::TrackState::Recording;
|
initial_state.columns[2].tracks[0].volume = 0.7;
|
||||||
initial_state.cells[2][3].volume = 0.6;
|
initial_state.columns[2].tracks[3].state = osc::TrackState::Recording;
|
||||||
|
initial_state.columns[2].tracks[3].volume = 0.6;
|
||||||
|
|
||||||
initial_state
|
initial_state
|
||||||
};
|
};
|
||||||
@ -53,7 +54,7 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state_receiver(&self) -> watch::Receiver<osc::State> {
|
pub fn state_receiver(&self) -> watch::Receiver<osc::State<COLS, ROWS>> {
|
||||||
self.state_receiver.clone()
|
self.state_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +87,16 @@ impl Client {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_osc_data(&self, data: Bytes, current_state: &mut osc::State) -> Result<()> {
|
async fn handle_osc_data(
|
||||||
|
&self,
|
||||||
|
data: Bytes,
|
||||||
|
current_state: &mut osc::State<COLS, ROWS>,
|
||||||
|
) -> Result<()> {
|
||||||
let (_, osc_packet) = rosc::decoder::decode_udp(&data)
|
let (_, osc_packet) = rosc::decoder::decode_udp(&data)
|
||||||
.map_err(|e| anyhow!("Failed to decode OSC packet: {}", e))?;
|
.map_err(|e| anyhow!("Failed to decode OSC packet: {}", e))?;
|
||||||
|
|
||||||
if let Some(update) = osc::Message::from_osc_packet(osc_packet)? {
|
if let Some(update) = osc::Message::from_osc_packet(osc_packet)? {
|
||||||
update.apply_to_state(current_state);
|
current_state.update(&update);
|
||||||
|
|
||||||
// Send updated state through watch channel
|
// Send updated state through watch channel
|
||||||
if let Err(_) = self.state_sender.send(current_state.clone()) {
|
if let Err(_) = self.state_sender.send(current_state.clone()) {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ impl Pendulum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update pendulum with interpolated timing data and return current position
|
/// Update pendulum with interpolated timing data and return current position
|
||||||
///
|
///
|
||||||
/// Returns position on rail where 0.0 = left side, 1.0 = right side
|
/// Returns position on rail where 0.0 = left side, 1.0 = right side
|
||||||
pub fn update(&mut self, interpolated: &Interpolated) -> f32 {
|
pub fn update(&mut self, interpolated: &Interpolated) -> f32 {
|
||||||
// Change direction on beat boundary
|
// Change direction on beat boundary
|
||||||
@ -37,4 +37,4 @@ impl Default for Pendulum {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
pub struct App {
|
pub struct App<const COLS: usize, const ROWS: usize> {
|
||||||
state_receiver: tokio::sync::watch::Receiver<osc::State>,
|
state_receiver: tokio::sync::watch::Receiver<osc::State<COLS, ROWS>>,
|
||||||
interpolation: crate::interpolation::Interpolation,
|
interpolation: crate::interpolation::Interpolation,
|
||||||
pendulum: crate::pendulum::Pendulum,
|
pendulum: crate::pendulum::Pendulum,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl<const COLS: usize, const ROWS: usize> App<COLS, ROWS> {
|
||||||
pub fn new(state_receiver: tokio::sync::watch::Receiver<osc::State>) -> Self {
|
pub fn new(state_receiver: tokio::sync::watch::Receiver<osc::State<COLS, ROWS>>) -> Self {
|
||||||
let interpolation = crate::interpolation::Interpolation::new(std::time::Instant::now());
|
let interpolation = crate::interpolation::Interpolation::new(std::time::Instant::now());
|
||||||
let pendulum = crate::pendulum::Pendulum::new();
|
let pendulum = crate::pendulum::Pendulum::new();
|
||||||
Self {
|
Self {
|
||||||
@ -16,7 +16,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl<const COLS: usize, const ROWS: usize> eframe::App for App<COLS, ROWS> {
|
||||||
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 interpolated = self.interpolation.update(&state, std::time::Instant::now());
|
||||||
@ -31,8 +31,8 @@ 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(pendulum_position));
|
ui.add(super::Metronome::new(pendulum_position));
|
||||||
ui.add(super::Matrix::new(
|
ui.add(super::Matrix::<COLS, ROWS>::new(
|
||||||
&state.cells,
|
&state.columns,
|
||||||
state.selected_column,
|
state.selected_column,
|
||||||
state.selected_row,
|
state.selected_row,
|
||||||
));
|
));
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
use super::ui_rows_ext::UiRowsExt;
|
use super::ui_rows_ext::UiRowsExt;
|
||||||
pub struct Column<'a> {
|
|
||||||
state: &'a Vec<osc::Track>,
|
pub struct Column<'a, const ROWS: usize> {
|
||||||
|
tracks: &'a [osc::Track; ROWS],
|
||||||
selected_row: Option<usize>,
|
selected_row: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Column<'a> {
|
impl<'a, const ROWS: usize> Column<'a, ROWS> {
|
||||||
pub fn new(state: &'a Vec<osc::Track>, selected_row: Option<usize>) -> Self {
|
pub fn new(tracks: &'a [osc::Track; ROWS], selected_row: Option<usize>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state,
|
tracks,
|
||||||
selected_row,
|
selected_row,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> egui::Widget for Column<'a> {
|
impl<'a, const ROWS: usize> egui::Widget for Column<'a, ROWS> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
ui.rows(self.state.len(), |ui| {
|
ui.rows(self.tracks.len(), |ui| {
|
||||||
for (i, track) in self.state.iter().enumerate() {
|
for (i, track) in self.tracks.iter().enumerate() {
|
||||||
ui[i].add(super::Track::new(track, Some(i) == self.selected_row));
|
ui[i].add(super::Track::new(track, Some(i) == self.selected_row));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,33 +1,33 @@
|
|||||||
pub struct Matrix<'a> {
|
pub struct Matrix<'a, const COLS: usize, const ROWS: usize> {
|
||||||
cells: &'a Vec<Vec<osc::Track>>,
|
columns: &'a [osc::Column<ROWS>; COLS],
|
||||||
selected_column: usize,
|
selected_column: usize,
|
||||||
selected_row: usize,
|
selected_row: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Matrix<'a> {
|
impl<'a, const COLS: usize, const ROWS: usize> Matrix<'a, COLS, ROWS> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
cells: &'a Vec<Vec<osc::Track>>,
|
columns: &'a [osc::Column<ROWS>; COLS],
|
||||||
selected_column: usize,
|
selected_column: usize,
|
||||||
selected_row: usize,
|
selected_row: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cells,
|
columns,
|
||||||
selected_column,
|
selected_column,
|
||||||
selected_row,
|
selected_row,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> egui::Widget for Matrix<'a> {
|
impl<'a, const COLS: usize, const ROWS: usize> egui::Widget for Matrix<'a, COLS, ROWS> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
ui.columns(self.cells.len(), |ui| {
|
ui.columns(self.columns.len(), |ui| {
|
||||||
for (i, column) in self.cells.iter().enumerate() {
|
for (i, column) in self.columns.iter().enumerate() {
|
||||||
let selected_row = if i == self.selected_column {
|
let selected_row = if i == self.selected_column {
|
||||||
Some(self.selected_row)
|
Some(self.selected_row)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
ui[i].add(super::Column::new(column, selected_row));
|
ui[i].add(super::Column::new(&column.tracks, selected_row));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.response()
|
ui.response()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use error::AddressParseResult;
|
|||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub use error::Result;
|
pub use error::Result;
|
||||||
pub use message::Message;
|
pub use message::Message;
|
||||||
|
pub use state::Column;
|
||||||
pub use state::State;
|
pub use state::State;
|
||||||
pub use state::Track;
|
pub use state::Track;
|
||||||
pub use state::TrackState;
|
pub use state::TrackState;
|
||||||
|
|||||||
@ -24,6 +24,14 @@ pub enum Message {
|
|||||||
Tempo {
|
Tempo {
|
||||||
bpm: f32,
|
bpm: f32,
|
||||||
},
|
},
|
||||||
|
ColumnBeatsChanged {
|
||||||
|
column: usize,
|
||||||
|
beats: usize,
|
||||||
|
},
|
||||||
|
ColumnBeatChanged {
|
||||||
|
column: usize,
|
||||||
|
beat: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
@ -37,7 +45,10 @@ impl Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_to_state(&self, state: &mut State) {
|
pub fn apply_to_state<const COLS: usize, const ROWS: usize>(
|
||||||
|
&self,
|
||||||
|
state: &mut State<COLS, ROWS>,
|
||||||
|
) {
|
||||||
match self {
|
match self {
|
||||||
Message::TrackStateChanged {
|
Message::TrackStateChanged {
|
||||||
column,
|
column,
|
||||||
@ -65,6 +76,12 @@ impl Message {
|
|||||||
Message::Tempo { bpm } => {
|
Message::Tempo { bpm } => {
|
||||||
state.set_tempo(*bpm);
|
state.set_tempo(*bpm);
|
||||||
}
|
}
|
||||||
|
Message::ColumnBeatsChanged { column, beats } => {
|
||||||
|
state.set_column_beats(*column, *beats);
|
||||||
|
}
|
||||||
|
Message::ColumnBeatChanged { column, beat } => {
|
||||||
|
state.set_column_beat(*column, *beat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +100,7 @@ impl Message {
|
|||||||
|
|
||||||
if let Some(rosc::OscType::String(state_str)) = args.first() {
|
if let Some(rosc::OscType::String(state_str)) = args.first() {
|
||||||
let state = state_str.parse::<TrackState>().unwrap_or(TrackState::Empty);
|
let state = state_str.parse::<TrackState>().unwrap_or(TrackState::Empty);
|
||||||
log::trace!("TrackStateChanged: column={column}, row={row}, state={state}");
|
log::debug!("TrackStateChanged: column={column}, row={row}, state={state}");
|
||||||
return Ok(Some(Message::TrackStateChanged { column, row, state }));
|
return Ok(Some(Message::TrackStateChanged { column, row, state }));
|
||||||
}
|
}
|
||||||
} else if addr.starts_with("/looper/cell/") && addr.ends_with("/volume") {
|
} else if addr.starts_with("/looper/cell/") && addr.ends_with("/volume") {
|
||||||
@ -97,23 +114,53 @@ impl Message {
|
|||||||
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
let row: usize = parts[4].parse::<usize>().address_part_result("row")? - 1; // Convert to 0-based
|
||||||
|
|
||||||
if let Some(rosc::OscType::Float(volume)) = args.first() {
|
if let Some(rosc::OscType::Float(volume)) = args.first() {
|
||||||
log::trace!("TrackVolumeChanged: column={column}, row={row}, volume={volume}");
|
log::debug!("TrackVolumeChanged: column={column}, row={row}, volume={volume}");
|
||||||
return Ok(Some(Message::TrackVolumeChanged {
|
return Ok(Some(Message::TrackVolumeChanged {
|
||||||
column,
|
column,
|
||||||
row,
|
row,
|
||||||
volume: *volume,
|
volume: *volume,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
} else if addr.starts_with("/looper/column/") && addr.ends_with("/beats") {
|
||||||
|
// Parse: /looper/column/{column}/beats
|
||||||
|
let parts: Vec<&str> = addr.split('/').collect();
|
||||||
|
if parts.len() != 5 {
|
||||||
|
return Err(Error::AddressParseError(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let column: usize = parts[3].parse::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||||
|
|
||||||
|
if let Some(rosc::OscType::Int(beats)) = args.first() {
|
||||||
|
log::debug!("ColumnBeatsChanged: column={column}, beats={beats}");
|
||||||
|
return Ok(Some(Message::ColumnBeatsChanged {
|
||||||
|
column,
|
||||||
|
beats: *beats as usize,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if addr.starts_with("/looper/column/") && addr.ends_with("/beat") {
|
||||||
|
// Parse: /looper/column/{column}/beat
|
||||||
|
let parts: Vec<&str> = addr.split('/').collect();
|
||||||
|
if parts.len() != 5 {
|
||||||
|
return Err(Error::AddressParseError(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let column: usize = parts[3].parse::<usize>().address_part_result("column")? - 1; // Convert to 0-based
|
||||||
|
|
||||||
|
if let Some(rosc::OscType::Int(beat)) = args.first() {
|
||||||
|
let beat = (*beat as usize).saturating_sub(1); // Convert to 0-based
|
||||||
|
log::debug!("ColumnBeatChanged: column={column}, beat={beat}");
|
||||||
|
return Ok(Some(Message::ColumnBeatChanged { column, beat }));
|
||||||
|
}
|
||||||
} else if addr == "/looper/selected/column" {
|
} else if addr == "/looper/selected/column" {
|
||||||
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
if let Some(rosc::OscType::Int(column_1based)) = args.first() {
|
||||||
log::trace!("SelectedColumnChanged: column={column_1based}");
|
|
||||||
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
let column = (*column_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
|
log::debug!("SelectedColumnChanged: column={column}");
|
||||||
return Ok(Some(Message::SelectedColumnChanged { column }));
|
return Ok(Some(Message::SelectedColumnChanged { column }));
|
||||||
}
|
}
|
||||||
} else if addr == "/looper/selected/row" {
|
} else if addr == "/looper/selected/row" {
|
||||||
if let Some(rosc::OscType::Int(row_1based)) = args.first() {
|
if let Some(rosc::OscType::Int(row_1based)) = args.first() {
|
||||||
log::trace!("SelectedRowChanged: row={row_1based}");
|
|
||||||
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
let row = (*row_1based as usize).saturating_sub(1); // Convert to 0-based
|
||||||
|
log::debug!("SelectedRowChanged: row={row}");
|
||||||
return Ok(Some(Message::SelectedRowChanged { row }));
|
return Ok(Some(Message::SelectedRowChanged { row }));
|
||||||
}
|
}
|
||||||
} else if addr == "/looper/metronome/position" {
|
} else if addr == "/looper/metronome/position" {
|
||||||
@ -125,7 +172,7 @@ impl Message {
|
|||||||
}
|
}
|
||||||
} else if addr == "/looper/tempo" {
|
} else if addr == "/looper/tempo" {
|
||||||
if let Some(rosc::OscType::Float(bpm)) = args.first() {
|
if let Some(rosc::OscType::Float(bpm)) = args.first() {
|
||||||
log::trace!("Tempo: bpm={bpm}");
|
log::debug!("Tempo: bpm={bpm}");
|
||||||
return Ok(Some(Message::Tempo { bpm: *bpm }));
|
return Ok(Some(Message::Tempo { bpm: *bpm }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,6 +234,22 @@ impl Message {
|
|||||||
args: vec![rosc::OscType::Float(bpm)],
|
args: vec![rosc::OscType::Float(bpm)],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Message::ColumnBeatsChanged { column, beats } => {
|
||||||
|
let address = format!("/looper/column/{}/beats", column + 1); // 1-based indexing
|
||||||
|
|
||||||
|
rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
|
addr: address,
|
||||||
|
args: vec![rosc::OscType::Int(beats as i32)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Message::ColumnBeatChanged { column, beat } => {
|
||||||
|
let address = format!("/looper/column/{}/beat", column + 1); // 1-based indexing
|
||||||
|
|
||||||
|
rosc::OscPacket::Message(rosc::OscMessage {
|
||||||
|
addr: address,
|
||||||
|
args: vec![rosc::OscType::Int((beat + 1) as i32)], // Convert to 1-based
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
osc/src/state.rs
115
osc/src/state.rs
@ -15,37 +15,41 @@ pub struct Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct Column<const ROWS: usize> {
|
||||||
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
pub tracks: [Track; ROWS],
|
||||||
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
pub beat_count: usize, // total beats in column (0 = no beats set)
|
||||||
pub cells: Vec<Vec<Track>>,
|
pub current_beat: usize, // current beat position (0-based internally)
|
||||||
pub columns: usize,
|
|
||||||
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 {
|
#[derive(Debug, Clone)]
|
||||||
pub fn new(columns: usize, rows: usize, tempo: f32) -> Self {
|
pub struct State<const COLS: usize, const ROWS: usize> {
|
||||||
let cells = (0..columns)
|
pub selected_column: usize, // 0-based (converted to 1-based for OSC)
|
||||||
.map(|_| {
|
pub selected_row: usize, // 0-based (converted to 1-based for OSC)
|
||||||
vec![
|
pub columns: [Column<ROWS>; COLS],
|
||||||
Track {
|
pub metronome_position: f32, // 0.0 - 1.0 position within current beat
|
||||||
state: TrackState::Empty,
|
pub metronome_timestamp: std::time::Instant, // When position was last updated
|
||||||
volume: 1.0
|
pub tempo: f32, // BPM
|
||||||
};
|
}
|
||||||
rows
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
|
impl<const ROWS: usize> Column<ROWS> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tracks: std::array::from_fn(|_| Track {
|
||||||
|
state: TrackState::Empty,
|
||||||
|
volume: 1.0,
|
||||||
|
}),
|
||||||
|
beat_count: 0,
|
||||||
|
current_beat: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const COLS: usize, const ROWS: usize> State<COLS, ROWS> {
|
||||||
|
pub fn new(tempo: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
selected_column: 0,
|
selected_column: 0,
|
||||||
selected_row: 0,
|
selected_row: 0,
|
||||||
cells,
|
columns: std::array::from_fn(|_| Column::new()),
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
metronome_position: 0.0,
|
metronome_position: 0.0,
|
||||||
metronome_timestamp: std::time::Instant::now(),
|
metronome_timestamp: std::time::Instant::now(),
|
||||||
tempo,
|
tempo,
|
||||||
@ -55,8 +59,8 @@ impl State {
|
|||||||
pub fn update(&mut self, message: &Message) {
|
pub fn update(&mut self, message: &Message) {
|
||||||
match message {
|
match message {
|
||||||
Message::TrackStateChanged { column, row, state } => {
|
Message::TrackStateChanged { column, row, state } => {
|
||||||
if *column < self.columns && *row < self.rows {
|
if *column < COLS && *row < ROWS {
|
||||||
self.cells[*column][*row].state = state.clone();
|
self.columns[*column].tracks[*row].state = state.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::TrackVolumeChanged {
|
Message::TrackVolumeChanged {
|
||||||
@ -64,17 +68,17 @@ impl State {
|
|||||||
row,
|
row,
|
||||||
volume,
|
volume,
|
||||||
} => {
|
} => {
|
||||||
if *column < self.columns && *row < self.rows {
|
if *column < COLS && *row < ROWS {
|
||||||
self.cells[*column][*row].volume = *volume;
|
self.columns[*column].tracks[*row].volume = *volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SelectedColumnChanged { column } => {
|
Message::SelectedColumnChanged { column } => {
|
||||||
if *column < self.columns {
|
if *column < COLS {
|
||||||
self.selected_column = *column;
|
self.selected_column = *column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SelectedRowChanged { row } => {
|
Message::SelectedRowChanged { row } => {
|
||||||
if *row < self.rows {
|
if *row < ROWS {
|
||||||
self.selected_row = *row;
|
self.selected_row = *row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +88,12 @@ impl State {
|
|||||||
Message::Tempo { bpm } => {
|
Message::Tempo { bpm } => {
|
||||||
self.set_tempo(*bpm);
|
self.set_tempo(*bpm);
|
||||||
}
|
}
|
||||||
|
Message::ColumnBeatsChanged { column, beats } => {
|
||||||
|
self.set_column_beats(*column, *beats);
|
||||||
|
}
|
||||||
|
Message::ColumnBeatChanged { column, beat } => {
|
||||||
|
self.set_column_beat(*column, *beat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,9 +111,20 @@ impl State {
|
|||||||
row: self.selected_row,
|
row: self.selected_row,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send all cell states and volumes
|
// Send column beat information and cell states/volumes
|
||||||
for (column, column_cells) in self.cells.iter().enumerate() {
|
for (column, column_data) in self.columns.iter().enumerate() {
|
||||||
for (row, track) in column_cells.iter().enumerate() {
|
// Send column beat info
|
||||||
|
messages.push(Message::ColumnBeatsChanged {
|
||||||
|
column,
|
||||||
|
beats: column_data.beat_count,
|
||||||
|
});
|
||||||
|
messages.push(Message::ColumnBeatChanged {
|
||||||
|
column,
|
||||||
|
beat: column_data.current_beat,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send all cell states and volumes for this column
|
||||||
|
for (row, track) in column_data.tracks.iter().enumerate() {
|
||||||
messages.push(Message::TrackStateChanged {
|
messages.push(Message::TrackStateChanged {
|
||||||
column,
|
column,
|
||||||
row,
|
row,
|
||||||
@ -121,26 +142,26 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_track_state(&mut self, column: usize, row: usize, state: TrackState) {
|
pub fn set_track_state(&mut self, column: usize, row: usize, state: TrackState) {
|
||||||
if column < self.columns && row < self.rows {
|
if column < COLS && row < ROWS {
|
||||||
self.cells[column][row].state = state;
|
self.columns[column].tracks[row].state = state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_column(&mut self, column: usize) {
|
pub fn set_selected_column(&mut self, column: usize) {
|
||||||
if column < self.columns {
|
if column < COLS {
|
||||||
self.selected_column = column;
|
self.selected_column = column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_row(&mut self, row: usize) {
|
pub fn set_selected_row(&mut self, row: usize) {
|
||||||
if row < self.rows {
|
if row < ROWS {
|
||||||
self.selected_row = row;
|
self.selected_row = row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_track_volume(&mut self, column: usize, row: usize, volume: f32) {
|
pub fn set_track_volume(&mut self, column: usize, row: usize, volume: f32) {
|
||||||
if column < self.columns && row < self.rows {
|
if column < COLS && row < ROWS {
|
||||||
self.cells[column][row].volume = volume;
|
self.columns[column].tracks[row].volume = volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,4 +173,16 @@ impl State {
|
|||||||
pub fn set_tempo(&mut self, bpm: f32) {
|
pub fn set_tempo(&mut self, bpm: f32) {
|
||||||
self.tempo = bpm;
|
self.tempo = bpm;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
pub fn set_column_beats(&mut self, column: usize, beats: usize) {
|
||||||
|
if column < COLS {
|
||||||
|
self.columns[column].beat_count = beats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_column_beat(&mut self, column: usize, beat: usize) {
|
||||||
|
if column < COLS {
|
||||||
|
self.columns[column].current_beat = beat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user