Add unit test for sync issues

This commit is contained in:
geens 2025-08-19 21:32:13 +02:00
parent 3e4f79380c
commit 477e94c7f6
13 changed files with 897 additions and 418 deletions

View File

@ -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<Item = jack::RawMidi<'_>> {
self.midi_input_events.clone().into_iter()
}

View File

@ -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]

View File

@ -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,33 +223,25 @@ 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);
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 {
for sample in output_slice {
*sample *= volume;
}
samples_written += samples_to_copy;
logical_position += samples_to_copy;
// Handle looping
if logical_position >= length {
logical_position = 0;
}
}
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());
}
}

View File

@ -155,7 +155,11 @@ impl<const ROWS: usize> Column<ROWS> {
&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<const ROWS: usize> Column<ROWS> {
// 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

View File

@ -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> {

View File

@ -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>),

View File

@ -132,7 +132,7 @@ async fn main() {
} else {
jack::Control::Continue
}
})
}),
)
.expect("Could not activate Jack");

View File

@ -58,7 +58,8 @@ impl Metronome {
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");
@ -278,13 +279,17 @@ impl Metronome {
// 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;
}
}
}
@ -348,7 +353,8 @@ mod tests {
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();
@ -366,17 +372,20 @@ mod tests {
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));
}
@ -390,7 +399,8 @@ mod tests {
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);
@ -411,7 +421,8 @@ mod tests {
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);
@ -427,7 +438,8 @@ mod tests {
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();
}
@ -448,7 +460,8 @@ mod tests {
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
@ -472,7 +485,8 @@ mod tests {
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
@ -498,7 +512,8 @@ mod tests {
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)
@ -528,7 +543,8 @@ mod tests {
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
@ -536,7 +552,8 @@ mod tests {
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)
@ -558,7 +575,8 @@ mod tests {
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
@ -581,7 +599,8 @@ mod tests {
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
@ -601,7 +620,8 @@ mod tests {
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
@ -621,7 +641,8 @@ mod tests {
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());
}
@ -639,12 +660,14 @@ mod tests {
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);
@ -661,7 +684,8 @@ mod tests {
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();
}));
@ -676,7 +700,8 @@ mod tests {
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));
@ -699,7 +724,8 @@ mod tests {
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)
@ -717,11 +743,13 @@ mod tests {
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();
@ -744,11 +772,13 @@ mod tests {
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();
@ -764,11 +794,13 @@ mod tests {
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();
@ -786,13 +818,15 @@ mod tests {
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)
@ -817,7 +851,8 @@ mod tests {
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
@ -840,10 +875,12 @@ mod tests {
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();
@ -851,19 +888,24 @@ mod tests {
// 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);
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();
@ -883,10 +925,12 @@ mod tests {
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();
@ -896,14 +940,17 @@ mod tests {
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();
@ -925,7 +972,8 @@ mod tests {
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
@ -933,7 +981,10 @@ mod tests {
// 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]
@ -945,7 +996,8 @@ mod tests {
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
@ -955,7 +1007,8 @@ 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, 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)
@ -980,7 +1033,8 @@ mod tests {
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
@ -988,6 +1042,9 @@ mod tests {
// 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 })
));
}
}

View File

@ -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<F, COLS, ROWS>,
) -> 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()));
}
}

View File

@ -185,7 +185,8 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
.process(audio_backend, &timing, &controllers)?;
for i in 0..audio_backend.audio_output().len() {
audio_backend.audio_output()[i] = audio_backend.audio_output()[i] + (audio_backend.audio_input()[i] * self.last_volume_setting);
audio_backend.audio_output()[i] = audio_backend.audio_output()[i]
+ (audio_backend.audio_input()[i] * self.last_volume_setting);
}
Ok(())
@ -247,9 +248,7 @@ mod tests {
const BUFFER_SIZE: usize = 10;
// Setup
let chunk_factory = MockFactory::new(
(0..50).map(|_| AudioChunk::allocate(1024)).collect()
);
let chunk_factory = MockFactory::new((0..50).map(|_| AudioChunk::allocate(1024)).collect());
let beep = Arc::new(AudioChunk {
samples: vec![1.0; 1].into_boxed_slice(),
@ -265,7 +264,8 @@ mod tests {
..Default::default()
};
let (mut post_record_handler, post_record_controller) = PostRecordHandler::new(&Args::default()).unwrap();
let (mut post_record_handler, post_record_controller) =
PostRecordHandler::new(&Args::default()).unwrap();
let (osc_tx, _osc_rx) = kanal::bounded(64);
let osc = OscController::new(osc_tx);
@ -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
);
}
}

View File

@ -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 => {
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,
playback_position,
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);
}
}
}