beat synced actions for track

This commit is contained in:
Geens 2025-06-07 20:53:37 +02:00
parent 9e6bd37b8f
commit 675216eb71
3 changed files with 431 additions and 100 deletions

View File

@ -45,6 +45,10 @@ pub fn process_events<F: ChunkFactory>(
// Button 2: Play/Mute // Button 2: Play/Mute
process_handler.play_toggle()?; process_handler.play_toggle()?;
} }
24 => {
// Button 5: Clear track
process_handler.clear_track()?;
}
_ => { _ => {
// Other CC messages - ignore for now // Other CC messages - ignore for now
} }

View File

@ -1,5 +1,6 @@
use crate::*; use crate::*;
use crate::metronome::Metronome; use crate::metronome::Metronome;
use crate::track::{TrackTiming, TrackState};
pub struct ProcessHandler<F: ChunkFactory> { pub struct ProcessHandler<F: ChunkFactory> {
track: Track, track: Track,
@ -27,105 +28,167 @@ impl<F: ChunkFactory> ProcessHandler<F> {
/// Handle record/play toggle button (Button 1) /// Handle record/play toggle button (Button 1)
pub fn record_toggle(&mut self) -> Result<()> { pub fn record_toggle(&mut self) -> Result<()> {
match self.track.state() { self.track.queue_record_toggle();
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);
}
}
Ok(()) Ok(())
} }
/// Handle play/mute toggle button (Button 2) /// Handle play/mute toggle button (Button 2)
pub fn play_toggle(&mut self) -> Result<()> { pub fn play_toggle(&mut self) -> Result<()> {
match self.track.state() { self.track.queue_play_toggle();
TrackState::Idle | TrackState::Recording => { Ok(())
if self.track.len() > 0 {
self.track.set_state(TrackState::Playing);
self.playback_position = 0;
}
}
TrackState::Playing => {
self.track.set_state(TrackState::Idle);
}
} }
/// Handle clear button (Button 5)
pub fn clear_track(&mut self) -> Result<()> {
self.track.queue_clear();
Ok(()) Ok(())
} }
} }
impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> { impl<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
fn process(&mut self, client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { 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) { if let Err(e) = midi::process_events(self, ps) {
log::error!("Error processing MIDI events: {}", e); log::error!("Error processing MIDI events: {}", e);
return jack::Control::Quit; return jack::Control::Quit;
} }
if let Err(e) = self.metronome.process(ps, &mut self.ports) { // 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); log::error!("Error processing metronome: {}", e);
return jack::Control::Quit; 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);
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; let buffer_size = client.buffer_size() as usize;
} let state_before = self.track.current_state().clone();
TrackState::Playing => {
// Playback // Calculate timing information for track processing
let sample_count_to_play = jack_buffer_size - index; let timing = self.calculate_track_timing(beat_sample_index, buffer_size, &state_before);
let sample_count_to_play =
sample_count_to_play.min(self.track.len() - self.playback_position); // Process track audio with calculated timing
if let Err(e) = self if let Err(e) = self.track.process(
.track ps,
.copy_samples( &mut self.ports,
&mut output_buffer[index..(index + sample_count_to_play)], timing,
self.playback_position, &mut self.chunk_factory
) ) {
{ log::error!("Error processing track: {}", e);
log::error!("Error copying samples: {}", e);
return jack::Control::Quit; return jack::Control::Quit;
} }
index += sample_count_to_play;
self.playback_position += sample_count_to_play; // Update playback position based on what happened
if self.playback_position >= self.track.len() { self.update_playback_position(beat_sample_index, buffer_size, &state_before);
self.playback_position = 0;
}
}
TrackState::Idle => {
// Idle - output silence
output_buffer[index..jack_buffer_size].fill(0.0);
break;
}
}
}
jack::Control::Continue jack::Control::Continue
} }
} }
impl<F: ChunkFactory> ProcessHandler<F> {
/// Calculate timing information for track processing
fn calculate_track_timing(
&self,
beat_sample_index: Option<u32>,
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<u32>,
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();
}
}
}

View File

@ -1,57 +1,321 @@
use crate::*; use crate::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum TrackState { pub enum TrackState {
Idle, Empty, // No audio data (---)
Playing, Idle, // Has data, not playing (READY)
Recording, 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 { pub struct Track {
audio_chunks: Arc<AudioChunk>, audio_chunks: Option<Arc<AudioChunk>>,
length: usize, length: usize,
state: TrackState, current_state: TrackState,
next_state: TrackState,
volume: f32,
} }
impl Track { impl Track {
pub fn new<F: ChunkFactory>(chunk_factory: &mut F) -> Result<Self> { pub fn new<F: ChunkFactory>(chunk_factory: &mut F) -> Result<Self> {
Ok(Self { Ok(Self {
audio_chunks: chunk_factory.create_chunk()?, audio_chunks: None,
length: 0, 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<F: ChunkFactory>(
&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<F: ChunkFactory>(
&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<F: ChunkFactory>(
&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<F: ChunkFactory>( pub fn append_samples<F: ChunkFactory>(
&mut self, &mut self,
samples: &[f32], samples: &[f32],
chunk_factory: &mut F, chunk_factory: &mut F,
) -> Result<()> { ) -> Result<()> {
self.audio_chunks.append_samples(samples, chunk_factory)?; 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(); self.length += samples.len();
}
Ok(()) Ok(())
} }
/// Clear all audio data from the track
fn clear_audio_data<F: ChunkFactory>(
&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 { pub fn len(&self) -> usize {
self.length self.length
} }
pub fn clear<F: ChunkFactory>(&mut self, chunk_factory: &mut F) -> Result<()> { pub fn volume(&self) -> f32 {
self.audio_chunks = chunk_factory.create_chunk()?; self.volume
self.length = 0;
self.state = TrackState::Idle;
Ok(())
} }
pub fn copy_samples(&self, dest: &mut [f32], start: usize) -> Result<()> { pub fn set_volume(&mut self, volume: f32) {
self.audio_chunks.copy_samples(dest, start) self.volume = volume.clamp(0.0, 1.0);
} }
pub fn state(&self) -> &TrackState { /// Handle record/play toggle command (sets next_state)
&self.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) { /// Handle play/mute toggle command (sets next_state)
self.state = 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;
} }
} }