From 675216eb71959fc8c272b65836ff3f06edb77722 Mon Sep 17 00:00:00 2001 From: Geens Date: Sat, 7 Jun 2025 20:53:37 +0200 Subject: [PATCH] beat synced actions for track --- src/midi.rs | 4 + src/process_handler.rs | 221 ++++++++++++++++++----------- src/track.rs | 306 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 431 insertions(+), 100 deletions(-) diff --git a/src/midi.rs b/src/midi.rs index 4f97f11..6dd25cf 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -45,6 +45,10 @@ pub fn process_events( // Button 2: Play/Mute process_handler.play_toggle()?; } + 24 => { + // Button 5: Clear track + process_handler.clear_track()?; + } _ => { // Other CC messages - ignore for now } diff --git a/src/process_handler.rs b/src/process_handler.rs index 6065b7c..f2d9622 100644 --- a/src/process_handler.rs +++ b/src/process_handler.rs @@ -1,5 +1,6 @@ use crate::*; use crate::metronome::Metronome; +use crate::track::{TrackTiming, TrackState}; pub struct ProcessHandler { track: Track, @@ -27,105 +28,167 @@ impl ProcessHandler { /// Handle record/play toggle button (Button 1) pub fn record_toggle(&mut self) -> Result<()> { - match self.track.state() { - TrackState::Idle => { - // Clear previous recording and start new recording - self.track.clear(&mut self.chunk_factory)?; - self.playback_position = 0; - self.track.set_state(TrackState::Recording); - } - TrackState::Recording => { - self.track.set_state(TrackState::Playing); - self.playback_position = 0; - } - TrackState::Playing => { - self.track.set_state(TrackState::Idle); - } - } + self.track.queue_record_toggle(); Ok(()) } /// Handle play/mute toggle button (Button 2) pub fn play_toggle(&mut self) -> Result<()> { - match self.track.state() { - TrackState::Idle | TrackState::Recording => { - if self.track.len() > 0 { - self.track.set_state(TrackState::Playing); - self.playback_position = 0; - } - } - TrackState::Playing => { - self.track.set_state(TrackState::Idle); - } - } + self.track.queue_play_toggle(); + Ok(()) + } + + /// Handle clear button (Button 5) + pub fn clear_track(&mut self) -> Result<()> { + self.track.queue_clear(); Ok(()) } } impl jack::ProcessHandler for ProcessHandler { fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { - // Process MIDI first + // Process MIDI first - this updates next_state on the track if let Err(e) = midi::process_events(self, ps) { log::error!("Error processing MIDI events: {}", e); return jack::Control::Quit; } - if let Err(e) = self.metronome.process(ps, &mut self.ports) { - log::error!("Error processing metronome: {}", e); + // Process metronome and get beat timing information + let beat_sample_index = match self.metronome.process(ps, &mut self.ports) { + Ok(beat_index) => beat_index, + Err(e) => { + log::error!("Error processing metronome: {}", e); + return jack::Control::Quit; + } + }; + + let buffer_size = client.buffer_size() as usize; + let state_before = self.track.current_state().clone(); + + // Calculate timing information for track processing + let timing = self.calculate_track_timing(beat_sample_index, buffer_size, &state_before); + + // Process track audio with calculated timing + if let Err(e) = self.track.process( + ps, + &mut self.ports, + timing, + &mut self.chunk_factory + ) { + log::error!("Error processing track: {}", e); return jack::Control::Quit; } - let input_buffer = self.ports.audio_in.as_slice(ps); - let output_buffer = self.ports.audio_out.as_mut_slice(ps); + // Update playback position based on what happened + self.update_playback_position(beat_sample_index, buffer_size, &state_before); - let jack_buffer_size = client.buffer_size() as usize; - - let mut index = 0; - - while index < jack_buffer_size { - match self.track.state() { - TrackState::Recording => { - // Recording - continue until manually stopped - let sample_count_to_append = jack_buffer_size - index; - let samples_to_append = &input_buffer[index..index + sample_count_to_append]; - if let Err(e) = self - .track - .append_samples(samples_to_append, &mut self.chunk_factory) - { - log::error!("Error appending samples: {}", e); - return jack::Control::Quit; - }; - output_buffer[index..(index + sample_count_to_append)].fill(0.0); - index += sample_count_to_append; - } - TrackState::Playing => { - // Playback - let sample_count_to_play = jack_buffer_size - index; - let sample_count_to_play = - sample_count_to_play.min(self.track.len() - self.playback_position); - if let Err(e) = self - .track - .copy_samples( - &mut output_buffer[index..(index + sample_count_to_play)], - self.playback_position, - ) - { - log::error!("Error copying samples: {}", e); - return jack::Control::Quit; - } - index += sample_count_to_play; - self.playback_position += sample_count_to_play; - if self.playback_position >= self.track.len() { - self.playback_position = 0; - } - } - TrackState::Idle => { - // Idle - output silence - output_buffer[index..jack_buffer_size].fill(0.0); - break; - } - } - } jack::Control::Continue } } + +impl ProcessHandler { + /// Calculate timing information for track processing + fn calculate_track_timing( + &self, + beat_sample_index: Option, + buffer_size: usize, + state_before: &TrackState, + ) -> TrackTiming { + match beat_sample_index { + None => { + // No beat in this buffer + TrackTiming::NoBeat { + position: self.playback_position, + } + } + Some(beat_index) => { + let beat_index = beat_index as usize; + let pre_beat_position = self.playback_position; + let post_beat_position = self.calculate_post_beat_position(state_before); + + TrackTiming::Beat { + pre_beat_position, + post_beat_position, + beat_sample_index: beat_index, + } + } + } + } + + /// Calculate the correct playback position after a beat transition + fn calculate_post_beat_position(&self, state_before: &TrackState) -> usize { + let state_after = self.track.next_state(); // Use next_state since transition hasn't happened yet + + match (state_before, state_after) { + (_, TrackState::Playing) if *state_before != TrackState::Playing => { + // Just started playing - start from beginning + // Note: In future Column implementation, this will be: + // column.get_sync_position() to sync with other playing tracks + 0 + } + (TrackState::Playing, TrackState::Playing) => { + // Continue playing - use current position + self.playback_position + } + _ => { + // Not playing after transition - position doesn't matter + self.playback_position + } + } + } + + /// Update playback position after track processing + fn update_playback_position( + &mut self, + beat_sample_index: Option, + buffer_size: usize, + state_before: &TrackState, + ) { + let state_after = self.track.current_state().clone(); + + match beat_sample_index { + None => { + // No beat - simple position update + if *state_before == TrackState::Playing { + self.advance_playback_position(buffer_size); + } + } + Some(beat_index) => { + let beat_index = beat_index as usize; + + // Handle position updates around beat boundary + if beat_index > 0 && *state_before == TrackState::Playing { + // Advance position for samples before beat + self.advance_playback_position(beat_index); + } + + // Check if state transition at beat affects position + if state_after == TrackState::Playing && *state_before != TrackState::Playing { + // Started playing at beat - reset position to post-beat calculation + self.playback_position = self.calculate_post_beat_position(state_before); + } + + // Advance position for samples after beat if playing + if beat_index < buffer_size && state_after == TrackState::Playing { + let samples_after_beat = buffer_size - beat_index; + self.advance_playback_position(samples_after_beat); + } + } + } + } + + /// Advance playback position with looping + fn advance_playback_position(&mut self, samples: usize) { + if self.track.len() == 0 { + self.playback_position = 0; + return; + } + + self.playback_position += samples; + + // Handle looping + while self.playback_position >= self.track.len() { + self.playback_position -= self.track.len(); + } + } +} diff --git a/src/track.rs b/src/track.rs index b29c363..9214acc 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,57 +1,321 @@ use crate::*; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum TrackState { - Idle, - Playing, - Recording, + Empty, // No audio data (---) + Idle, // Has data, not playing (READY) + Playing, // Currently playing (PLAY) + Recording, // Currently recording (REC) +} + +#[derive(Debug)] +pub enum TrackTiming { + NoBeat { + position: usize + }, + Beat { + pre_beat_position: usize, + post_beat_position: usize, + beat_sample_index: usize, + } } pub struct Track { - audio_chunks: Arc, + audio_chunks: Option>, length: usize, - state: TrackState, + current_state: TrackState, + next_state: TrackState, + volume: f32, } impl Track { pub fn new(chunk_factory: &mut F) -> Result { Ok(Self { - audio_chunks: chunk_factory.create_chunk()?, + audio_chunks: None, length: 0, - state: TrackState::Idle, + current_state: TrackState::Empty, + next_state: TrackState::Empty, + volume: 1.0, }) } + /// Main audio processing method called from ProcessHandler + pub fn process( + &mut self, + ps: &jack::ProcessScope, + ports: &mut JackPorts, + timing: TrackTiming, + chunk_factory: &mut F, + ) -> Result<()> { + let input_buffer = ports.audio_in.as_slice(ps); + let output_buffer = ports.audio_out.as_mut_slice(ps); + let buffer_size = output_buffer.len(); + + match timing { + TrackTiming::NoBeat { position } => { + // No beat in this buffer - process entire buffer with current state + self.process_audio_range( + input_buffer, + output_buffer, + 0, + buffer_size, + position, + chunk_factory, + )?; + } + TrackTiming::Beat { + pre_beat_position, + post_beat_position, + beat_sample_index + } => { + if beat_sample_index > 0 { + // Process samples before beat with current state + self.process_audio_range( + input_buffer, + output_buffer, + 0, + beat_sample_index, + pre_beat_position, + chunk_factory, + )?; + } + + // Apply state transition at beat boundary + self.apply_state_transition(chunk_factory)?; + + if beat_sample_index < buffer_size { + // Process samples after beat with new current state + self.process_audio_range( + input_buffer, + output_buffer, + beat_sample_index, + buffer_size, + post_beat_position, + chunk_factory, + )?; + } + } + } + + Ok(()) + } + + /// Process audio for a specific range within the buffer + fn process_audio_range( + &mut self, + input_buffer: &[f32], + output_buffer: &mut [f32], + start_index: usize, + end_index: usize, + playback_position: usize, + chunk_factory: &mut F, + ) -> Result<()> { + let sample_count = end_index - start_index; + if sample_count == 0 { + return Ok(()); + } + + match self.current_state { + TrackState::Empty | TrackState::Idle => { + // Output silence for this range + output_buffer[start_index..end_index].fill(0.0); + } + TrackState::Recording => { + // Record input samples + let samples_to_record = &input_buffer[start_index..end_index]; + self.append_samples(samples_to_record, chunk_factory)?; + + // Output silence during recording + output_buffer[start_index..end_index].fill(0.0); + } + TrackState::Playing => { + // Playback with looping + self.copy_samples_to_output( + &mut output_buffer[start_index..end_index], + sample_count, + playback_position, + )?; + } + } + + Ok(()) + } + + /// Apply state transition from next_state to current_state + fn apply_state_transition( + &mut self, + chunk_factory: &mut F + ) -> Result<()> { + // Handle transitions that require setup + match (&self.current_state, &self.next_state) { + (_, TrackState::Recording) => { + // Starting to record - clear previous data and create new chunk + self.clear_audio_data(chunk_factory)?; + } + (_, TrackState::Playing) => { + // Starting playback - check if we have audio data + if self.audio_chunks.is_none() || self.length == 0 { + // No audio data - transition to Idle instead + self.next_state = TrackState::Idle; + } + } + (_, TrackState::Empty) => { + // Clear operation - remove audio data + // Note: Actual deallocation will happen later via IO thread + self.audio_chunks = None; + self.length = 0; + } + _ => { + // Other transitions don't require special handling + } + } + + // Apply the state transition + self.current_state = self.next_state.clone(); + + Ok(()) + } + + /// Copy samples from track audio to output buffer with looping + fn copy_samples_to_output( + &self, + output_slice: &mut [f32], + sample_count: usize, + mut playback_position: usize, + ) -> Result<()> { + if let Some(ref audio_chunks) = self.audio_chunks { + if self.length == 0 { + output_slice.fill(0.0); + return Ok(()); + } + + let mut samples_written = 0; + + while samples_written < sample_count { + let samples_remaining = sample_count - samples_written; + let samples_until_loop = self.length - playback_position; + let samples_to_copy = samples_remaining.min(samples_until_loop); + + // Copy directly from audio chunks to output slice + audio_chunks.copy_samples( + &mut output_slice[samples_written..samples_written + samples_to_copy], + playback_position, + )?; + + // Apply volume scaling in-place + for sample in &mut output_slice[samples_written..samples_written + samples_to_copy] { + *sample *= self.volume; + } + + samples_written += samples_to_copy; + playback_position += samples_to_copy; + + // Handle looping + if playback_position >= self.length { + playback_position = 0; + } + } + } else { + output_slice.fill(0.0); + } + + Ok(()) + } + + /// Append samples to the track's audio data pub fn append_samples( &mut self, samples: &[f32], chunk_factory: &mut F, ) -> Result<()> { - self.audio_chunks.append_samples(samples, chunk_factory)?; - self.length += samples.len(); + if self.audio_chunks.is_none() { + self.audio_chunks = Some(chunk_factory.create_chunk()?); + } + + if let Some(ref mut chunks) = self.audio_chunks { + chunks.append_samples(samples, chunk_factory)?; + self.length += samples.len(); + } + Ok(()) } + /// Clear all audio data from the track + fn clear_audio_data( + &mut self, + chunk_factory: &mut F, + ) -> Result<()> { + self.audio_chunks = Some(chunk_factory.create_chunk()?); + self.length = 0; + Ok(()) + } + + // Public accessors and commands for MIDI handling + pub fn current_state(&self) -> &TrackState { + &self.current_state + } + + pub fn next_state(&self) -> &TrackState { + &self.next_state + } + + pub fn set_next_state(&mut self, state: TrackState) { + self.next_state = state; + } + pub fn len(&self) -> usize { self.length } - pub fn clear(&mut self, chunk_factory: &mut F) -> Result<()> { - self.audio_chunks = chunk_factory.create_chunk()?; - self.length = 0; - self.state = TrackState::Idle; - Ok(()) + pub fn volume(&self) -> f32 { + self.volume } - pub fn copy_samples(&self, dest: &mut [f32], start: usize) -> Result<()> { - self.audio_chunks.copy_samples(dest, start) + pub fn set_volume(&mut self, volume: f32) { + self.volume = volume.clamp(0.0, 1.0); } - pub fn state(&self) -> &TrackState { - &self.state + /// Handle record/play toggle command (sets next_state) + pub fn queue_record_toggle(&mut self) { + match self.current_state { + TrackState::Empty | TrackState::Idle => { + self.next_state = TrackState::Recording; + } + TrackState::Recording => { + self.next_state = TrackState::Playing; + } + TrackState::Playing => { + self.next_state = TrackState::Idle; + } + } } - pub fn set_state(&mut self, state: TrackState) { - self.state = state; + /// Handle play/mute toggle command (sets next_state) + pub fn queue_play_toggle(&mut self) { + match self.current_state { + TrackState::Empty => { + // Can't play empty track + self.next_state = TrackState::Empty; + } + TrackState::Idle => { + if self.length > 0 { + self.next_state = TrackState::Playing; + } else { + self.next_state = TrackState::Idle; + } + } + TrackState::Recording => { + // Don't change state while recording + self.next_state = TrackState::Recording; + } + TrackState::Playing => { + self.next_state = TrackState::Idle; + } + } + } + + /// Handle clear command (sets next_state) + pub fn queue_clear(&mut self) { + self.next_state = TrackState::Empty; } }