diff --git a/audio_engine/src/args.rs b/audio_engine/src/args.rs index d11b7bb..b873d6c 100644 --- a/audio_engine/src/args.rs +++ b/audio_engine/src/args.rs @@ -40,6 +40,6 @@ fn default_config_path() -> String { #[cfg(test)] impl Default for Args { fn default() -> Self { - Self::parse_from([std::ffi::OsString::new();0]) + Self::parse_from([std::ffi::OsString::new(); 0]) } -} \ No newline at end of file +} diff --git a/audio_engine/src/audio_backend.rs b/audio_engine/src/audio_backend.rs index a94b02c..25008c2 100644 --- a/audio_engine/src/audio_backend.rs +++ b/audio_engine/src/audio_backend.rs @@ -1,5 +1,5 @@ use crate::{JackPorts, Result as LooperResult}; -use jack::{Frames, RawMidi, ProcessScope}; +use jack::{Frames, ProcessScope, RawMidi}; /// A thin abstraction over the data provided by the audio backend for one process cycle. /// This allows the `ProcessHandler`'s core logic to be tested without a live JACK server. @@ -51,7 +51,8 @@ impl<'a> AudioBackend<'a> for JackAudioBackend<'a> { fn midi_output(&mut self, time: u32, bytes: &[u8]) -> LooperResult<()> { let mut writer = self.ports.midi_out.writer(self.scope); - writer.write(&RawMidi { time, bytes }) + writer + .write(&RawMidi { time, bytes }) .map_err(|_| crate::LooperError::Midi(std::panic::Location::caller())) } } @@ -88,7 +89,6 @@ impl<'a> AudioBackend<'a> for MockAudioBackend<'a> { } fn midi_input(&self) -> impl Iterator> { - self.midi_input_events.clone().into_iter() } @@ -96,4 +96,4 @@ impl<'a> AudioBackend<'a> for MockAudioBackend<'a> { self.midi_output_events.push((time, bytes.to_vec())); Ok(()) } -} \ No newline at end of file +} diff --git a/audio_engine/src/audio_chunk.rs b/audio_engine/src/audio_chunk.rs index 37f0b28..a099251 100644 --- a/audio_engine/src/audio_chunk.rs +++ b/audio_engine/src/audio_chunk.rs @@ -59,7 +59,7 @@ impl AudioChunk { chunk_factory: &mut F, ) -> Result<()> { let mut_self = Arc::get_mut(self) - .ok_or(LooperError::ChunkOwnership(std::panic::Location::caller()))?; + .ok_or(LooperError::AudioDataState(std::panic::Location::caller()))?; if let Some(next) = &mut mut_self.next { // Not the last chunk, recurse next.append_samples(samples, chunk_factory) @@ -110,14 +110,14 @@ impl AudioChunk { .copy_from_slice(&self.samples[start..self.sample_count]); next.copy_samples(&mut dest[sample_count_from_this_chunk..], 0)?; } else { - return Err(LooperError::OutOfBounds(std::panic::Location::caller())); + return Err(LooperError::AudioDataState(std::panic::Location::caller())); } Ok(()) } else if let Some(next) = &self.next { // Copy from next chunk next.copy_samples(dest, start - self.sample_count) } else { - Err(LooperError::OutOfBounds(std::panic::Location::caller())) + Err(LooperError::AudioDataState(std::panic::Location::caller())) } } } @@ -483,7 +483,7 @@ mod tests { assert!(result.is_err()); assert!(matches!( result.unwrap_err(), - LooperError::ChunkOwnership(_) + LooperError::AudioDataState(_) )); // Chunk should be unchanged @@ -550,7 +550,10 @@ mod tests { let result = chunk.copy_samples(&mut dest, 5); // Start beyond sample_count assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); + assert!(matches!( + result.unwrap_err(), + LooperError::AudioDataState(_) + )); } #[test] @@ -565,7 +568,10 @@ mod tests { let result = chunk.copy_samples(&mut dest, 1); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); + assert!(matches!( + result.unwrap_err(), + LooperError::AudioDataState(_) + )); } #[test] @@ -645,7 +651,10 @@ mod tests { let result = chunk1.copy_samples(&mut dest, 10); // Start beyond all chunks assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); + assert!(matches!( + result.unwrap_err(), + LooperError::AudioDataState(_) + )); } #[test] diff --git a/audio_engine/src/audio_data.rs b/audio_engine/src/audio_data.rs index 2d93767..3da69c0 100644 --- a/audio_engine/src/audio_data.rs +++ b/audio_engine/src/audio_data.rs @@ -63,12 +63,12 @@ impl AudioData { *length += samples.len(); Ok(()) } - _ => Err(LooperError::OutOfBounds(std::panic::Location::caller())), + _ => Err(LooperError::AudioDataState(std::panic::Location::caller())), } } /// Copy samples to output buffer with volume scaling and looping - /// logical_position is the position within the column cycle (0 = beat 1) + /// logical_position is the position within the column cycle (0 = first beat, first sample) pub fn copy_samples_to_output( &self, output_slice: &mut [f32], @@ -106,7 +106,7 @@ impl AudioData { sync_offset, .. } => Ok((chunks.clone(), *sync_offset)), - _ => Err(LooperError::ChunkOwnership(std::panic::Location::caller())), + _ => Err(LooperError::AudioDataState(std::panic::Location::caller())), } } @@ -117,12 +117,12 @@ impl AudioData { length: old_length, .. } => { if buffer.len() != *old_length { - return Err(LooperError::OutOfBounds(std::panic::Location::caller())); + return Err(LooperError::AudioDataState(std::panic::Location::caller())); } *self = Self::Consolidated { buffer }; Ok(()) } - _ => Err(LooperError::OutOfBounds(std::panic::Location::caller())), + _ => Err(LooperError::AudioDataState(std::panic::Location::caller())), } } @@ -159,7 +159,7 @@ impl AudioData { sync_offset: usize, length: usize, output_slice: &mut [f32], - mut logical_position: usize, + logical_position: usize, volume: f32, ) -> Result<()> { if length == 0 { @@ -167,15 +167,26 @@ impl AudioData { return Ok(()); } + // Check if the logical position is beyond the available data + if logical_position >= length { + return Err(LooperError::AudioDataState(std::panic::Location::caller())); + } + + // Check if the requested samples would go beyond the available data + if logical_position + output_slice.len() > length { + return Err(LooperError::AudioDataState(std::panic::Location::caller())); + } + let mut samples_written = 0; let samples_needed = output_slice.len(); + let mut current_logical_position = logical_position; while samples_written < samples_needed { // Map logical position to buffer position using sync offset - let buffer_position = (logical_position + sync_offset) % length; + let buffer_position = (current_logical_position + length - sync_offset) % length; let samples_remaining_in_output = samples_needed - samples_written; - // This is the crucial change: determine how many contiguous samples can be read from the current buffer_position + // Determine how many contiguous samples can be read from the current buffer_position let readable_samples_from_here = length - buffer_position; let samples_to_copy = samples_remaining_in_output.min(readable_samples_from_here); @@ -192,7 +203,7 @@ impl AudioData { } samples_written += samples_to_copy; - logical_position = (logical_position + samples_to_copy) % length; + current_logical_position = (current_logical_position + samples_to_copy) % length; } Ok(()) @@ -203,7 +214,7 @@ impl AudioData { &self, buffer: &[f32], output_slice: &mut [f32], - mut logical_position: usize, + logical_position: usize, volume: f32, ) -> Result<()> { let length = buffer.len(); @@ -212,31 +223,23 @@ impl AudioData { return Ok(()); } - let mut samples_written = 0; - let samples_needed = output_slice.len(); + // Check if the logical position is beyond the available data + if logical_position >= length { + return Err(LooperError::AudioDataState(std::panic::Location::caller())); + } - while samples_written < samples_needed { - let samples_remaining = samples_needed - samples_written; - let samples_until_loop = length - logical_position; - let samples_to_copy = samples_remaining.min(samples_until_loop); + // Check if the requested samples would go beyond the available data + if logical_position + output_slice.len() > length { + return Err(LooperError::AudioDataState(std::panic::Location::caller())); + } - // Direct copy since consolidated buffer is already reordered - let src = &buffer[logical_position..logical_position + samples_to_copy]; - let dest = &mut output_slice[samples_written..samples_written + samples_to_copy]; - dest.copy_from_slice(src); + // Direct copy since consolidated buffer is already reordered + let src = &buffer[logical_position..logical_position + output_slice.len()]; + output_slice.copy_from_slice(src); - // Apply volume scaling in-place - for sample in dest { - *sample *= volume; - } - - samples_written += samples_to_copy; - logical_position += samples_to_copy; - - // Handle looping - if logical_position >= length { - logical_position = 0; - } + // Apply volume scaling in-place + for sample in output_slice { + *sample *= volume; } Ok(()) @@ -334,18 +337,15 @@ mod tests { let sync_offset = 2; let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap(); - // Recording order: [A, B, C, D] but recorded starting at beat 3 (sync_offset=2) - // So logical mapping should be: beat1=C, beat2=D, beat3=A, beat4=B - let samples = vec![10.0, 20.0, 30.0, 40.0]; // A, B, C, D + let samples = vec![10.0, 20.0, 30.0, 40.0, 50.0]; audio_data.append_samples(&samples, &mut factory).unwrap(); - let mut output = vec![0.0; 4]; + let mut output = vec![0.0; 5]; audio_data .copy_samples_to_output(&mut output, 0, 1.0) - .unwrap(); // Start at logical beat 1 + .unwrap(); - // Should get: [C, D, A, B] = [30.0, 40.0, 10.0, 20.0] - assert_eq!(output, vec![30.0, 40.0, 10.0, 20.0]); + assert_eq!(output, vec![40.0, 50.0, 10.0, 20.0, 30.0]); } #[test] @@ -364,43 +364,6 @@ mod tests { assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]); } - #[test] - fn test_copy_samples_unconsolidated_looping() { - let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); - let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap(); - - let samples = vec![1.0, 2.0]; - audio_data.append_samples(&samples, &mut factory).unwrap(); - - let mut output = vec![0.0; 6]; // Request more samples than available - audio_data - .copy_samples_to_output(&mut output, 0, 1.0) - .unwrap(); - - // Should loop: [1.0, 2.0, 1.0, 2.0, 1.0, 2.0] - assert_eq!(output, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]); - } - - #[test] - fn test_copy_samples_unconsolidated_offset_with_looping() { - let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); - let sync_offset = 1; - let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap(); - - // Recording: [A, B, C] starting at beat 2 (sync_offset=1) - // Logical mapping: beat1=B, beat2=C, beat3=A - let samples = vec![10.0, 20.0, 30.0]; - audio_data.append_samples(&samples, &mut factory).unwrap(); - - let mut output = vec![0.0; 6]; // Request 2 full loops - audio_data - .copy_samples_to_output(&mut output, 0, 1.0) - .unwrap(); - - // Should get: [B, C, A, B, C, A] = [20.0, 30.0, 10.0, 20.0, 30.0, 10.0] - assert_eq!(output, vec![20.0, 30.0, 10.0, 20.0, 30.0, 10.0]); - } - #[test] fn test_copy_samples_consolidated() { // Create consolidated data directly @@ -443,7 +406,7 @@ mod tests { #[test] fn test_set_consolidated_buffer() { let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); - let mut audio_data = AudioData::new_unconsolidated(&mut factory, 100).unwrap(); + let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap(); let samples = vec![1.0, 2.0, 3.0, 4.0]; audio_data.append_samples(&samples, &mut factory).unwrap(); @@ -498,4 +461,55 @@ mod tests { _ => panic!("Expected Empty variant after clear"), } } + + #[test] + fn test_copy_samples_unconsolidated_overflow() { + let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); + let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap(); + + let samples = vec![1.0, 2.0]; + audio_data.append_samples(&samples, &mut factory).unwrap(); + + let mut output = vec![0.0; 3]; + let res = audio_data.copy_samples_to_output(&mut output, 0, 1.0); + + assert!(res.is_err()); + } + + #[test] + fn test_copy_samples_unconsolidated_overflow_offset() { + let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); + let sync_offset = 1; + let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap(); + + let samples = vec![10.0, 20.0]; + audio_data.append_samples(&samples, &mut factory).unwrap(); + + let mut output = vec![0.0; 2]; + let res = audio_data.copy_samples_to_output(&mut output, 1, 1.0); + + assert!(res.is_err()); + } + + #[test] + fn test_copy_samples_consolidated_overflow() { + let buffer = vec![10.0, 20.0].into_boxed_slice(); + let audio_data = AudioData::Consolidated { buffer }; + + let mut output = vec![0.0; 3]; + let res = audio_data.copy_samples_to_output(&mut output, 0, 1.0); + + assert!(res.is_err()); + } + + #[test] + fn test_copy_samples_consolidated_overflow_offset() { + let buffer = vec![10.0, 20.0].into_boxed_slice(); + let audio_data = AudioData::Consolidated { buffer }; + + let mut output = vec![0.0; 2]; + let res = audio_data.copy_samples_to_output(&mut output, 1, 1.0); + + assert!(res.is_err()); + } } diff --git a/audio_engine/src/column.rs b/audio_engine/src/column.rs index ab92719..40e8e8a 100644 --- a/audio_engine/src/column.rs +++ b/audio_engine/src/column.rs @@ -155,7 +155,11 @@ impl Column { &track_controllers, )?; - for (output_val, scratch_pad_val) in audio_backend.audio_output().iter_mut().zip(scratch_pad.iter()) { + for (output_val, scratch_pad_val) in audio_backend + .audio_output() + .iter_mut() + .zip(scratch_pad.iter()) + { *output_val += *scratch_pad_val; } } @@ -169,7 +173,8 @@ impl Column { // Update playback position if new_len > 0 { - self.playback_position = (self.playback_position + audio_backend.audio_input().len()) % new_len; + self.playback_position = + (self.playback_position + audio_backend.audio_input().len()) % new_len; } else { // During recording of first track, don't use modulo - let position grow // so that beat calculation works correctly diff --git a/audio_engine/src/controllers.rs b/audio_engine/src/controllers.rs index 4e4da98..9df1fbe 100644 --- a/audio_engine/src/controllers.rs +++ b/audio_engine/src/controllers.rs @@ -8,11 +8,11 @@ pub struct MatrixControllers<'a> { } pub struct ColumnControllers<'a> { - post_record: &'a PostRecordController, - osc: &'a OscController, - persistence: &'a PersistenceManagerController, - sample_rate: usize, - column: usize, + pub(crate) post_record: &'a PostRecordController, + pub(crate) osc: &'a OscController, + pub(crate) persistence: &'a PersistenceManagerController, + pub(crate) sample_rate: usize, + pub(crate) column: usize, } pub struct TrackControllers<'a> { diff --git a/audio_engine/src/looper_error.rs b/audio_engine/src/looper_error.rs index 600e667..ec59207 100644 --- a/audio_engine/src/looper_error.rs +++ b/audio_engine/src/looper_error.rs @@ -7,10 +7,7 @@ pub enum LooperError { ChunkAllocation(&'static std::panic::Location<'static>), #[error("Cannot modify chunk with multiple references")] - ChunkOwnership(&'static std::panic::Location<'static>), - - #[error("Index out of bounds")] - OutOfBounds(&'static std::panic::Location<'static>), + AudioDataState(&'static std::panic::Location<'static>), #[error("Failed to send OSC message")] Osc(&'static std::panic::Location<'static>), diff --git a/audio_engine/src/main.rs b/audio_engine/src/main.rs index 9f77f94..7bc474f 100644 --- a/audio_engine/src/main.rs +++ b/audio_engine/src/main.rs @@ -132,7 +132,7 @@ async fn main() { } else { jack::Control::Continue } - }) + }), ) .expect("Could not activate Jack"); @@ -217,4 +217,4 @@ impl jack::ProcessHandler fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { } } -*/ \ No newline at end of file +*/ diff --git a/audio_engine/src/metronome.rs b/audio_engine/src/metronome.rs index 2d7c6a0..ff1095e 100644 --- a/audio_engine/src/metronome.rs +++ b/audio_engine/src/metronome.rs @@ -42,7 +42,7 @@ impl Metronome { ) -> Result { let current_frame_time = audio_backend.last_frame_time(); let buffer_size = audio_backend.n_frames() as usize; - + // Clear the click output buffer first let click_output = audio_backend.click_output(); for sample in click_output.iter_mut() { @@ -51,39 +51,40 @@ impl Metronome { // Output MIDI messages self.output_midi_messages(audio_backend, current_frame_time, buffer_size)?; - + // Handle first-time initialization if self.last_frame_time.is_none() { self.last_frame_time = Some(current_frame_time); self.last_beat_time = Some(current_frame_time); - + // Check if there are multiple beats in the first buffer - let next_beat_after_first = current_frame_time.wrapping_add(self.frames_per_beat as u32); + let next_beat_after_first = + current_frame_time.wrapping_add(self.frames_per_beat as u32); let buffer_end = current_frame_time + buffer_size as u32 - 1; if next_beat_after_first <= buffer_end { panic!("Multiple beats in single buffer not supported"); } - + // First beat occurs at the start of the first buffer let timing = BufferTiming { beat_in_buffer: Some(0), missed_frames: 0, beat_in_missed: None, }; - + // Play click and send OSC message self.play_click_at_position(audio_backend, 0)?; osc_controller.metronome_position_changed(0.0)?; - + // Update state self.last_frame_time = Some(current_frame_time + buffer_size as u32 - 1); - + return Ok(timing); } - + let last_frame = self.last_frame_time.unwrap(); let expected_frame = last_frame + 1; - + // Check for xrun (audio discontinuity) let missed_frames = if current_frame_time != expected_frame { if current_frame_time > expected_frame { @@ -99,32 +100,32 @@ impl Metronome { } else { 0 }; - + let mut beat_in_buffer = None; let mut beat_in_missed = None; - + if missed_frames > 0 { // Handle xrun case - missed range excludes current buffer let missed_start = last_frame + 1; let missed_end = current_frame_time - 1; - + // Check if a beat occurred in the missed frames if let Some(last_beat) = self.last_beat_time { let next_beat_frame = last_beat.wrapping_add(self.frames_per_beat as u32); - + if next_beat_frame >= missed_start && next_beat_frame <= missed_end { // Verify only one beat was missed let beat_after_next = next_beat_frame.wrapping_add(self.frames_per_beat as u32); if beat_after_next <= missed_end { return Err(LooperError::Xrun(std::panic::Location::caller())); } - + beat_in_missed = Some((next_beat_frame - missed_start) as u32); self.last_beat_time = Some(next_beat_frame); - + // Send OSC message for missed beat osc_controller.metronome_position_changed(0.0)?; - + // Send SPP update for missed beat let beats_since_start = next_beat_frame / self.frames_per_beat as u32; let lsb = (beats_since_start & 0x7F) as u8; @@ -133,46 +134,46 @@ impl Metronome { } } } - + // Check for beat in current buffer if let Some(last_beat) = self.last_beat_time { let next_beat_frame = last_beat.wrapping_add(self.frames_per_beat as u32); let buffer_start = current_frame_time; let buffer_end = current_frame_time + buffer_size as u32 - 1; - + if next_beat_frame >= buffer_start && next_beat_frame <= buffer_end { // Verify only one beat in buffer let beat_after_next = next_beat_frame.wrapping_add(self.frames_per_beat as u32); if beat_after_next <= buffer_end { panic!("Multiple beats in single buffer not supported"); } - + let beat_position = (next_beat_frame - buffer_start) as u32; beat_in_buffer = Some(beat_position); self.last_beat_time = Some(next_beat_frame); - + // Play click and send OSC message self.play_click_at_position(audio_backend, beat_position as usize)?; osc_controller.metronome_position_changed(0.0)?; - - // Send SPP update - calculate beats since start (first beat was at frame 0 on first buffer) + + // Send SPP update - calculate beats since start (first beat was at frame 0 on first buffer) let beats_since_start = next_beat_frame / self.frames_per_beat as u32; - + let lsb = (beats_since_start & 0x7F) as u8; let msb = ((beats_since_start >> 7) & 0x7F) as u8; audio_backend.midi_output(beat_position, &[0xF2, lsb, msb])?; } } - - // Always check if we need to continue a click from previous buffer, + + // Always check if we need to continue a click from previous buffer, // unless we just started a new beat in this buffer if beat_in_buffer.is_none() { self.continue_click_playback(audio_backend, current_frame_time)?; } - + // Update frame time tracking self.last_frame_time = Some(current_frame_time + buffer_size as u32 - 1); - + Ok(BufferTiming { beat_in_buffer, missed_frames, @@ -204,7 +205,7 @@ impl Metronome { ) -> Result<()> { // Send Stop then Start on tempo change audio_backend.midi_output(0, &[0xFC])?; // Stop - audio_backend.midi_output(0, &[0xFA])?; // Start + audio_backend.midi_output(0, &[0xFA])?; // Start audio_backend.midi_output(0, &[0xF2, 0x00, 0x00])?; // SPP reset Ok(()) } @@ -216,17 +217,17 @@ impl Metronome { buffer_size: usize, ) -> Result<()> { let frames_per_midi_clock = self.frames_per_beat / 24; - + // Send Start message on first process call if self.last_frame_time.is_none() { audio_backend.midi_output(0, &[0xFA])?; // Start audio_backend.midi_output(0, &[0xF2, 0x00, 0x00])?; // SPP reset } - + // Calculate MIDI clock positions in current buffer let buffer_start = current_frame_time; let buffer_end = current_frame_time + buffer_size as u32 - 1; - + // Generate clocks for this buffer // Start from the first clock boundary at or after buffer_start let first_clock_in_buffer = if buffer_start % frames_per_midi_clock as u32 == 0 { @@ -234,7 +235,7 @@ impl Metronome { } else { ((buffer_start / frames_per_midi_clock as u32) + 1) * frames_per_midi_clock as u32 }; - + let mut clock_frame = first_clock_in_buffer; loop { if clock_frame <= buffer_end { @@ -245,10 +246,10 @@ impl Metronome { break; } } - + // SPP will be updated when beats are detected in the main process logic // This is handled after beat detection in the process method - + Ok(()) } @@ -259,11 +260,11 @@ impl Metronome { ) -> Result<()> { let click_output = audio_backend.click_output(); let samples_to_write = (self.click_samples.sample_count).min(click_output.len() - position); - + for i in 0..samples_to_write { click_output[position + i] = self.click_samples.samples[i] * self.click_volume; } - + Ok(()) } @@ -275,20 +276,24 @@ impl Metronome { // Check if we should continue a click from the previous buffer if let Some(last_beat) = self.last_beat_time { let frames_since_beat = current_frame_time.wrapping_sub(last_beat); - + // Only continue if the time difference makes sense (not a huge wraparound) // and we're still within the click duration - if frames_since_beat < self.click_samples.sample_count as u32 && frames_since_beat < 1000000 { + if frames_since_beat < self.click_samples.sample_count as u32 + && frames_since_beat < 1000000 + { let click_output = audio_backend.click_output(); let start_offset = frames_since_beat as usize; - let samples_to_write = (self.click_samples.sample_count - start_offset).min(click_output.len()); - + let samples_to_write = + (self.click_samples.sample_count - start_offset).min(click_output.len()); + for i in 0..samples_to_write { - click_output[i] += self.click_samples.samples[start_offset + i] * self.click_volume; + click_output[i] += + self.click_samples.samples[start_offset + i] * self.click_volume; } } } - + Ok(()) } } @@ -347,11 +352,12 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); - + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + let timing = metronome.process(&mut backend, &osc).unwrap(); - + assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(timing.missed_frames, 0); assert_eq!(timing.beat_in_missed, None); @@ -364,19 +370,22 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - + // First beat at frame 0 - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); assert_eq!(timing.beat_in_buffer, Some(0)); - + // No beat in next buffer (frames 50-99) - let mut backend = create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); assert_eq!(timing.beat_in_buffer, None); - + // Second beat at frame 100 (index 0 in buffer starting at frame 100) - let mut backend = create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); assert_eq!(timing.beat_in_buffer, Some(0)); } @@ -388,13 +397,14 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - + // Beat occurs at frame 125, buffer starts at frame 120 - let mut backend = create_mock_audio_backend(50, 120, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 120, &audio_input, &mut audio_output, &mut click_output); // Set up state where last beat was at frame 25, so next beat is at 25 + 100 = 125 metronome.last_frame_time = Some(119); metronome.last_beat_time = Some(25); - + let timing = metronome.process(&mut backend, &osc).unwrap(); assert_eq!(timing.beat_in_buffer, Some(5)); // Frame 125 - 120 = index 5 } @@ -406,14 +416,15 @@ mod tests { let audio_input = [0.0; 30]; let mut audio_output = [0.0; 30]; let mut click_output = [0.0; 30]; - + // Set up state where last beat was at frame 100, current buffer is frames 130-159 metronome.last_frame_time = Some(129); metronome.last_beat_time = Some(100); - - let mut backend = create_mock_audio_backend(30, 130, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(30, 130, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + assert_eq!(timing.beat_in_buffer, None); assert_eq!(timing.missed_frames, 0); } @@ -426,8 +437,9 @@ mod tests { let audio_input = [0.0; 200]; // Large buffer let mut audio_output = [0.0; 200]; let mut click_output = [0.0; 200]; - - let mut backend = create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); } @@ -443,14 +455,15 @@ mod tests { }; let mut metronome = Metronome::new(click_samples, &state); let (osc, _osc_rx) = create_osc_controller(); - + let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have click samples at the beginning assert_eq!(click_output[0], 0.8); assert_eq!(click_output[9], 0.8); @@ -467,14 +480,15 @@ mod tests { }; let mut metronome = Metronome::new(click_samples, &state); let (osc, _osc_rx) = create_osc_controller(); - + let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have scaled click samples assert_eq!(click_output[0], 0.4); // 0.8 * 0.5 } @@ -489,18 +503,19 @@ mod tests { }; let mut metronome = Metronome::new(click_samples, &state); let (osc, _osc_rx) = create_osc_controller(); - + // Set up for beat at frame 105, buffer starts at 100 metronome.last_frame_time = Some(99); metronome.last_beat_time = Some(5); - + let audio_input = [0.0; 20]; let mut audio_output = [0.0; 20]; let mut click_output = [0.0; 20]; - - let mut backend = create_mock_audio_backend(20, 100, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(20, 100, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Click should start at index 5 (frame 105 - 100) assert_eq!(click_output[4], 0.0); assert_eq!(click_output[5], 0.8); @@ -519,26 +534,28 @@ mod tests { }; let mut metronome = Metronome::new(click_samples, &state); let (osc, _osc_rx) = create_osc_controller(); - + // First buffer: beat starts at frame 95 (index 5 in buffer 90-99) metronome.last_frame_time = Some(89); metronome.last_beat_time = Some(95u32.wrapping_sub(100)); // Next beat = this + 100 = 95 - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - - let mut backend = create_mock_audio_backend(10, 90, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(10, 90, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have click starting at index 5 assert_eq!(click_output[5], 0.8); assert_eq!(click_output[9], 0.8); - + // Second buffer: continue click from previous buffer - let mut backend = create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should continue click from frame 5 onwards (index 0-9 should have click) assert_eq!(click_output[0], 0.8); assert_eq!(click_output[9], 0.8); @@ -548,19 +565,20 @@ mod tests { fn test_no_click_when_no_beat() { let mut metronome = create_test_metronome(100); let (osc, _osc_rx) = create_osc_controller(); - + // Set up state where no beat occurs in this buffer and last click has finished // Last beat was 150 frames ago, so click (which is 100 frames) has finished metronome.last_frame_time = Some(49); metronome.last_beat_time = Some(50u32.wrapping_sub(150)); - + let audio_input = [0.0; 30]; let mut audio_output = [0.0; 30]; let mut click_output = [0.0; 30]; - - let mut backend = create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // No click should be present assert!(click_output.iter().all(|&x| x == 0.0)); } @@ -571,19 +589,20 @@ mod tests { fn test_xrun_detection() { let mut metronome = create_test_metronome(100); let (osc, _osc_rx) = create_osc_controller(); - + // Set up normal operation first metronome.last_frame_time = Some(50); metronome.last_beat_time = Some(0); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // Jump from frame 50 to frame 150 (missed 90 frames) - let mut backend = create_mock_audio_backend(10, 150, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 150, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + assert_eq!(timing.missed_frames, 90); // 150 - 50 - 10 = 90 } @@ -591,19 +610,20 @@ mod tests { fn test_beat_in_missed_frames() { let mut metronome = create_test_metronome(100); let (osc, _osc_rx) = create_osc_controller(); - + // Set up: last frame was 50, last beat was at 0 metronome.last_frame_time = Some(50); metronome.last_beat_time = Some(0); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // Jump to frame 180, missing frames 60-169 (next beat at frame 100 was missed) - let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + assert_eq!(timing.missed_frames, 120); // 180 - 50 - 10 = 120 assert_eq!(timing.beat_in_missed, Some(49)); // Beat at frame 100, missed range starts at 51, so 100-51=49 } @@ -612,16 +632,17 @@ mod tests { fn test_multiple_beats_in_xrun_panics() { let mut metronome = create_test_metronome(50); // Fast tempo let (osc, _osc_rx) = create_osc_controller(); - + metronome.last_frame_time = Some(25); metronome.last_beat_time = Some(0); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // Jump from frame 25 to frame 200, missing multiple beats - let mut backend = create_mock_audio_backend(10, 200, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 200, &audio_input, &mut audio_output, &mut click_output); assert!(metronome.process(&mut backend, &osc).is_err()); } @@ -629,22 +650,24 @@ mod tests { fn test_recovery_after_xrun() { let mut metronome = create_test_metronome(100); let (osc, _osc_rx) = create_osc_controller(); - + // Set up xrun scenario metronome.last_frame_time = Some(50); metronome.last_beat_time = Some(0); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // First: xrun with missed beat - let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); assert!(timing.beat_in_missed.is_some()); - + // Second: normal operation should continue correctly - let mut backend = create_mock_audio_backend(10, 190, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 190, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); assert_eq!(timing.missed_frames, 0); assert_eq!(timing.beat_in_missed, None); @@ -659,9 +682,10 @@ mod tests { let audio_input = [0.0; 200]; // Buffer larger than frames_per_beat let mut audio_output = [0.0; 200]; let mut click_output = [0.0; 200]; - + // This should trigger the multiple beats panic - let mut backend = create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { metronome.process(&mut backend, &osc).unwrap(); })); @@ -675,10 +699,11 @@ mod tests { let audio_input = [0.0; 1]; let mut audio_output = [0.0; 1]; let mut click_output = [0.0; 1]; - - let mut backend = create_mock_audio_backend(1, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(1, 0, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(click_output[0], 0.4); // Should have click with default volume (0.8 * 0.5) } @@ -687,21 +712,22 @@ mod tests { fn test_frames_per_beat_change_resets_metronome() { let mut metronome = create_test_metronome(100); let (osc, _osc_rx) = create_osc_controller(); - + // Set up some state metronome.last_frame_time = Some(50); metronome.last_beat_time = Some(25); - + // Change frames per beat (simulating tempo change when no tracks playing) metronome.set_frames_per_beat(200); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - - let mut backend = create_mock_audio_backend(10, 60, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(10, 60, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + // Should behave as if starting fresh (first beat detection) assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(timing.missed_frames, 0); @@ -716,21 +742,23 @@ mod tests { let audio_input = [0.0; 30]; let mut audio_output = [0.0; 30]; let mut click_output = [0.0; 30]; - - let mut backend = create_mock_audio_backend(30, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(30, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Filter only clock messages (0xF8) - let clock_messages: Vec<_> = backend.midi_output_events + let clock_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xF8) .collect(); - + // Should have MIDI clock messages at 24 PPQN // 120 frames per beat / 24 = 5 frames per clock // In a 30-frame buffer, we should have 6 clock messages (at frames 0, 5, 10, 15, 20, 25) assert_eq!(clock_messages.len(), 6); - + // Check timing - should be at 5-frame intervals let times: Vec = clock_messages.iter().map(|(t, _)| *t).collect(); assert_eq!(times, vec![0, 5, 10, 15, 20, 25]); @@ -743,12 +771,14 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have MIDI start message (0xFA) at the beginning - let start_messages: Vec<_> = backend.midi_output_events + let start_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .collect(); @@ -763,12 +793,14 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have SPP reset message (0xF2, 0x00, 0x00) at the beginning - let spp_messages: Vec<_> = backend.midi_output_events + let spp_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .collect(); @@ -784,21 +816,23 @@ mod tests { let audio_input = [0.0; 20]; let mut audio_output = [0.0; 20]; let mut click_output = [0.0; 20]; - + // First buffer (frames 0-19) - let mut backend = create_mock_audio_backend(20, 0, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(20, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should have clocks at frames 0, 4, 8, 12, 16 - + // Second buffer (frames 20-39) - let mut backend = create_mock_audio_backend(20, 20, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(20, 20, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should continue with clocks at frames 0, 4, 8, 12, 16 (relative to buffer start) // Which are absolute frames 20, 24, 28, 32, 36 assert!(backend.midi_output_events.len() > 0); - + // Verify timing continuity let times: Vec = backend.midi_output_events.iter().map(|(t, _)| *t).collect(); assert_eq!(times, vec![0, 4, 8, 12, 16]); // Relative to buffer start frame 20 @@ -806,23 +840,24 @@ mod tests { #[test] fn test_midi_clock_handles_xrun() { - let mut metronome = create_test_metronome(48); // 48 frames per beat = 2 frames per clock + let mut metronome = create_test_metronome(48); // 48 frames per beat = 2 frames per clock let (osc, _osc_rx) = create_osc_controller(); - + // Set up normal operation first metronome.last_frame_time = Some(10); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // Jump from frame 10 to frame 100 (xrun, missed frames 11-89) - let mut backend = create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - + // Should still output MIDI clocks for the current buffer assert!(backend.midi_output_events.len() > 0); - + // All clock messages should be for frames within the current buffer (100-109) for (time, bytes) in &backend.midi_output_events { if bytes.len() == 1 && bytes[0] == 0xF8 { @@ -838,32 +873,39 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - + // First beat - should have SPP reset - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - - let spp_messages: Vec<_> = backend.midi_output_events + + let spp_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .collect(); assert_eq!(spp_messages[0].1, vec![0xF2, 0x00, 0x00]); - + // Process several buffers to get to the next beat (200 frames total) // Frames 50-99 - let mut backend = create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - // Frames 100-149 - let mut backend = create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output); + // Frames 100-149 + let mut backend = + create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); // Frames 150-199 - let mut backend = create_mock_audio_backend(50, 150, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 150, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); // Frames 200-249 (contains next beat at frame 200) - let mut backend = create_mock_audio_backend(50, 200, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 200, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - - let spp_messages: Vec<_> = backend.midi_output_events + + let spp_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .collect(); @@ -881,33 +923,38 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - + // First buffer should have Start message - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); metronome.process(&mut backend, &osc).unwrap(); - - let start_messages: Vec<_> = backend.midi_output_events + + let start_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .collect(); assert_eq!(start_messages.len(), 1); - + // Change tempo - this resets the metronome state metronome.set_frames_per_beat(200); - + // Send tempo change messages manually for this test - let mut backend = create_mock_audio_backend(50, 60, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(50, 60, &audio_input, &mut audio_output, &mut click_output); metronome.send_tempo_change_messages(&mut backend).unwrap(); - - let stop_messages: Vec<_> = backend.midi_output_events + + let stop_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFC) .collect(); - let start_messages: Vec<_> = backend.midi_output_events + let start_messages: Vec<_> = backend + .midi_output_events .iter() .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .collect(); - + assert_eq!(stop_messages.len(), 1); assert_eq!(start_messages.len(), 1); // Stop should come before Start (both at time 0) @@ -924,43 +971,49 @@ mod tests { let audio_input = [0.0; 50]; let mut audio_output = [0.0; 50]; let mut click_output = [0.0; 50]; - - let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); + + let mut backend = + create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + // Should have a beat and send OSC message assert_eq!(timing.beat_in_buffer, Some(0)); - + // Check OSC message was sent let msg = osc_rx.try_recv().unwrap(); - assert!(matches!(msg, Some(osc::Message::MetronomePosition { position: 0.0 }))); + assert!(matches!( + msg, + Some(osc::Message::MetronomePosition { position: 0.0 }) + )); } #[test] fn test_osc_message_not_sent_when_no_beat() { let mut metronome = create_test_metronome(100); let (osc, osc_rx) = create_osc_controller(); - + // First, process one buffer normally to initialize properly let audio_input1 = [0.0; 30]; let mut audio_output1 = [0.0; 30]; let mut click_output1 = [0.0; 30]; - let mut backend1 = create_mock_audio_backend(30, 0, &audio_input1, &mut audio_output1, &mut click_output1); + let mut backend1 = + create_mock_audio_backend(30, 0, &audio_input1, &mut audio_output1, &mut click_output1); metronome.process(&mut backend1, &osc).unwrap(); - + // Clear any messages from initialization let _ = osc_rx.try_recv(); - + // Now process a buffer where no beat occurs let audio_input = [0.0; 30]; let mut audio_output = [0.0; 30]; let mut click_output = [0.0; 30]; - let mut backend = create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + // Should have no beat (next beat would be at frame 100, buffer is 50-79) assert_eq!(timing.beat_in_buffer, None); - + // Check that the expected behavior occurred (may have OSC messages from xrun detection) // The core test is that no beat was detected in the buffer // OSC behavior for edge cases can vary based on implementation details @@ -970,24 +1023,28 @@ mod tests { fn test_osc_message_sent_on_xrun_beat() { let mut metronome = create_test_metronome(100); let (osc, osc_rx) = create_osc_controller(); - + // Set up: last frame was 50, last beat was at 0 metronome.last_frame_time = Some(50); metronome.last_beat_time = Some(0); - + let audio_input = [0.0; 10]; let mut audio_output = [0.0; 10]; let mut click_output = [0.0; 10]; - + // Jump to frame 180, missing frames 60-169 (next beat at frame 100 was missed) - let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); + let mut backend = + create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output); let timing = metronome.process(&mut backend, &osc).unwrap(); - + // Should have missed beat assert!(timing.beat_in_missed.is_some()); - + // Check OSC message was sent for the missed beat let msg = osc_rx.try_recv().unwrap(); - assert!(matches!(msg, Some(osc::Message::MetronomePosition { position: 0.0 }))); + assert!(matches!( + msg, + Some(osc::Message::MetronomePosition { position: 0.0 }) + )); } } diff --git a/audio_engine/src/midi.rs b/audio_engine/src/midi.rs index 637237d..3f5aed8 100644 --- a/audio_engine/src/midi.rs +++ b/audio_engine/src/midi.rs @@ -1,7 +1,13 @@ use crate::*; /// Process MIDI events -pub fn process_events<'a, A: AudioBackend<'a>, F: ChunkFactory, const COLS: usize, const ROWS: usize>( +pub fn process_events< + 'a, + A: AudioBackend<'a>, + F: ChunkFactory, + const COLS: usize, + const ROWS: usize, +>( audio_backend: &mut A, process_handler: &mut ProcessHandler, ) -> Result<()> { @@ -20,7 +26,7 @@ pub fn process_events<'a, A: AudioBackend<'a>, F: ChunkFactory, const COLS: usiz raw_events[event_count][2] = midi_event.bytes[2]; event_count += 1; } else { - return Err(LooperError::OutOfBounds(std::panic::Location::caller())); + return Err(LooperError::Midi(std::panic::Location::caller())); } } diff --git a/audio_engine/src/process_handler.rs b/audio_engine/src/process_handler.rs index 6444075..f200655 100644 --- a/audio_engine/src/process_handler.rs +++ b/audio_engine/src/process_handler.rs @@ -185,7 +185,8 @@ impl ProcessHandler::new( 48000, BUFFER_SIZE, @@ -282,12 +282,12 @@ mod tests { post_record_controller, osc, persistence, - ).unwrap(); + ) + .unwrap(); let audio_output_buffer = &mut [0.0; BUFFER_SIZE]; let click_output_buffer = &mut [0.0; BUFFER_SIZE]; - - + // Warm up, no commands here let mut last_frame_time = 0; let input = [last_frame_time as f32; BUFFER_SIZE]; @@ -301,12 +301,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Empty + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Press record last_frame_time = 10; @@ -321,12 +335,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Empty + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) starts last_frame_time = 20; @@ -341,12 +369,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) first beat continues last_frame_time = 30; @@ -361,12 +403,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) begin second beat last_frame_time = 40; @@ -381,12 +437,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) second beat continues last_frame_time = 50; @@ -401,12 +471,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) second beat continues last_frame_time = 60; @@ -421,12 +505,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Record track (0, 0) 3rd beat starts last_frame_time = 70; @@ -441,12 +539,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Press record end last_frame_time = 80; @@ -461,12 +573,26 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .all(|f| *f == last_frame_time as f32), + "expected to hear the input signal" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 }); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Recording { volume: 1.0 } + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Track (0, 0) stops recording, starts playing first beat last_frame_time = 90; @@ -481,13 +607,32 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == last_frame_time as f32), "expected to hear the input signal for the first part"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording started at frame 24"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == last_frame_time as f32), + "expected to hear the input signal for the first part" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 20.0), + "expected to hear the input + recording started at frame 24" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 0); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Press down last_frame_time = 100; @@ -502,13 +647,32 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording started at frame 24, second buffer"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 30.0), + "expected to hear the input + recording started at frame 24, second buffer" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 40.0), + "expected to hear the input + recording started at frame 24, 3rd buffer" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Press record last_frame_time = 110; @@ -523,13 +687,32 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording started at frame 24, 4th buffer"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 40.0), + "expected to hear the input + recording started at frame 24, 3rd buffer" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 50.0), + "expected to hear the input + recording started at frame 24, 4th buffer" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Empty + ); // Recording track (0, 1) starts recording at second column beat last_frame_time = 120; @@ -544,13 +727,36 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 50.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 60.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Recording track (0, 1) continues recording last_frame_time = 130; @@ -565,13 +771,36 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 60.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 70.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Track (0, 1) records for second beat, column is at 3rd beat last_frame_time = 140; @@ -586,13 +815,36 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 70.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 80.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Column beat 3 continues last_frame_time = 150; @@ -607,13 +859,36 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 80.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 90.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Column wraps to first beat, track (0, 1) records 3rd beat last_frame_time = 160; @@ -628,13 +903,36 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 90.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 20.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Continue playback and recording last_frame_time = 170; @@ -649,13 +947,30 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 30.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Continue playback and recording last_frame_time = 180; @@ -670,13 +985,30 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 40.0), + "expected to hear the input + recording of first track" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::RecordingAutoStop { + volume: 1.0, + target_samples: 72, + sync_offset: 24 + } + ); // Recording track (0, 1) auto stops, mixed playback starts last_frame_time = 190; @@ -691,17 +1023,37 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)"); - assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 50.0), + "expected to hear the input + recording of first track" + ); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 50.0 + 120.0), + "expected to hear the input + track (0, 0) + track (0, 1)" + ); + assert!( + click_output_buffer.iter().any(|f| *f == 1.0), + "expected click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Playing + ); // Process audio - let prh_result = tokio::time::timeout(std::time::Duration::from_secs(1), post_record_handler.run()).await; + let prh_result = + tokio::time::timeout(std::time::Duration::from_secs(1), post_record_handler.run()) + .await; assert!(prh_result.is_err(), "Expected timeout, got {prh_result:#?}"); // Playback consolidated @@ -717,12 +1069,25 @@ mod tests { midi_output_events: vec![], }; handler.process(&mut backend).unwrap(); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)"); - assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)"); - assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); + assert!( + audio_output_buffer + .iter() + .any(|f| *f == (last_frame_time as f32) + 60.0 + 130.0), + "expected to hear the input + track (0, 0) + track (0, 1)" + ); + assert!( + click_output_buffer.iter().all(|f| *f == 0.0), + "expected no click" + ); assert_eq!(handler.selected_column, 0); assert_eq!(handler.selected_row, 1); - assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); - assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing); + assert_eq!( + handler.track_matrix.columns[0].tracks[0].current_state, + TrackState::Playing + ); + assert_eq!( + handler.track_matrix.columns[0].tracks[1].current_state, + TrackState::Playing + ); } -} \ No newline at end of file +} diff --git a/audio_engine/src/track.rs b/audio_engine/src/track.rs index c68e8e3..a86a6bb 100644 --- a/audio_engine/src/track.rs +++ b/audio_engine/src/track.rs @@ -183,10 +183,7 @@ impl Track { // Process samples after beat with new current state if (beat_index_in_buffer as usize) < output_buffer.len() { // Calculate position after beat for remaining samples - let mut post_beat_position = playback_position + beat_index_in_buffer as usize; - if self.audio_data.len() > 0 { - post_beat_position %= self.audio_data.len(); - } + let post_beat_position = playback_position + beat_index_in_buffer as usize; self.process_audio_range( &audio_backend.audio_input()[beat_index_in_buffer as _..], @@ -244,11 +241,40 @@ impl Track { output_buffer.fill(0.0); } TrackState::Playing => { - self.audio_data.copy_samples_to_output( - output_buffer, - playback_position, - self.volume, - )?; + if self.audio_data.len() > 0 { + // Wrap playback position within the track's audio data length + let wrapped_position = playback_position % self.audio_data.len(); + + // Check if we need to wrap around during this read + if wrapped_position + output_buffer.len() <= self.audio_data.len() { + // Simple case: read fits entirely within the audio data + self.audio_data.copy_samples_to_output( + output_buffer, + wrapped_position, + self.volume, + )?; + } else { + // Complex case: need to wrap around during the read + let samples_until_end = self.audio_data.len() - wrapped_position; + + // Read first part (from wrapped_position to end of audio data) + self.audio_data.copy_samples_to_output( + &mut output_buffer[..samples_until_end], + wrapped_position, + self.volume, + )?; + + // Read second part (from start of audio data) + self.audio_data.copy_samples_to_output( + &mut output_buffer[samples_until_end..], + 0, + self.volume, + )?; + } + } else { + // No audio data to play, output silence + output_buffer.fill(0.0); + } } } diff --git a/audio_engine/src/track_matrix.rs b/audio_engine/src/track_matrix.rs index 49f9d1f..52401ca 100644 --- a/audio_engine/src/track_matrix.rs +++ b/audio_engine/src/track_matrix.rs @@ -90,4 +90,4 @@ impl TrackMatrix