beat synced actions for track
This commit is contained in:
parent
9e6bd37b8f
commit
675216eb71
@ -45,6 +45,10 @@ pub fn process_events<F: ChunkFactory>(
|
||||
// Button 2: Play/Mute
|
||||
process_handler.play_toggle()?;
|
||||
}
|
||||
24 => {
|
||||
// Button 5: Clear track
|
||||
process_handler.clear_track()?;
|
||||
}
|
||||
_ => {
|
||||
// Other CC messages - ignore for now
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use crate::*;
|
||||
use crate::metronome::Metronome;
|
||||
use crate::track::{TrackTiming, TrackState};
|
||||
|
||||
pub struct ProcessHandler<F: ChunkFactory> {
|
||||
track: Track,
|
||||
@ -27,105 +28,167 @@ impl<F: ChunkFactory> ProcessHandler<F> {
|
||||
|
||||
/// 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<F: ChunkFactory> jack::ProcessHandler for ProcessHandler<F> {
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
306
src/track.rs
306
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<AudioChunk>,
|
||||
audio_chunks: Option<Arc<AudioChunk>>,
|
||||
length: usize,
|
||||
state: TrackState,
|
||||
current_state: TrackState,
|
||||
next_state: TrackState,
|
||||
volume: f32,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
pub fn new<F: ChunkFactory>(chunk_factory: &mut F) -> Result<Self> {
|
||||
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<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>(
|
||||
&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<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 {
|
||||
self.length
|
||||
}
|
||||
|
||||
pub fn clear<F: ChunkFactory>(&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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user