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

@ -40,6 +40,6 @@ fn default_config_path() -> String {
#[cfg(test)] #[cfg(test)]
impl Default for Args { impl Default for Args {
fn default() -> Self { fn default() -> Self {
Self::parse_from([std::ffi::OsString::new();0]) Self::parse_from([std::ffi::OsString::new(); 0])
} }
} }

View File

@ -1,5 +1,5 @@
use crate::{JackPorts, Result as LooperResult}; 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. /// 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. /// 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<()> { fn midi_output(&mut self, time: u32, bytes: &[u8]) -> LooperResult<()> {
let mut writer = self.ports.midi_out.writer(self.scope); 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())) .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<'_>> { fn midi_input(&self) -> impl Iterator<Item = jack::RawMidi<'_>> {
self.midi_input_events.clone().into_iter() self.midi_input_events.clone().into_iter()
} }

View File

@ -59,7 +59,7 @@ impl AudioChunk {
chunk_factory: &mut F, chunk_factory: &mut F,
) -> Result<()> { ) -> Result<()> {
let mut_self = Arc::get_mut(self) 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 { if let Some(next) = &mut mut_self.next {
// Not the last chunk, recurse // Not the last chunk, recurse
next.append_samples(samples, chunk_factory) next.append_samples(samples, chunk_factory)
@ -110,14 +110,14 @@ impl AudioChunk {
.copy_from_slice(&self.samples[start..self.sample_count]); .copy_from_slice(&self.samples[start..self.sample_count]);
next.copy_samples(&mut dest[sample_count_from_this_chunk..], 0)?; next.copy_samples(&mut dest[sample_count_from_this_chunk..], 0)?;
} else { } else {
return Err(LooperError::OutOfBounds(std::panic::Location::caller())); return Err(LooperError::AudioDataState(std::panic::Location::caller()));
} }
Ok(()) Ok(())
} else if let Some(next) = &self.next { } else if let Some(next) = &self.next {
// Copy from next chunk // Copy from next chunk
next.copy_samples(dest, start - self.sample_count) next.copy_samples(dest, start - self.sample_count)
} else { } 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!(result.is_err());
assert!(matches!( assert!(matches!(
result.unwrap_err(), result.unwrap_err(),
LooperError::ChunkOwnership(_) LooperError::AudioDataState(_)
)); ));
// Chunk should be unchanged // Chunk should be unchanged
@ -550,7 +550,10 @@ mod tests {
let result = chunk.copy_samples(&mut dest, 5); // Start beyond sample_count let result = chunk.copy_samples(&mut dest, 5); // Start beyond sample_count
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); assert!(matches!(
result.unwrap_err(),
LooperError::AudioDataState(_)
));
} }
#[test] #[test]
@ -565,7 +568,10 @@ mod tests {
let result = chunk.copy_samples(&mut dest, 1); let result = chunk.copy_samples(&mut dest, 1);
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); assert!(matches!(
result.unwrap_err(),
LooperError::AudioDataState(_)
));
} }
#[test] #[test]
@ -645,7 +651,10 @@ mod tests {
let result = chunk1.copy_samples(&mut dest, 10); // Start beyond all chunks let result = chunk1.copy_samples(&mut dest, 10); // Start beyond all chunks
assert!(result.is_err()); assert!(result.is_err());
assert!(matches!(result.unwrap_err(), LooperError::OutOfBounds(_))); assert!(matches!(
result.unwrap_err(),
LooperError::AudioDataState(_)
));
} }
#[test] #[test]

View File

@ -63,12 +63,12 @@ impl AudioData {
*length += samples.len(); *length += samples.len();
Ok(()) 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 /// 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( pub fn copy_samples_to_output(
&self, &self,
output_slice: &mut [f32], output_slice: &mut [f32],
@ -106,7 +106,7 @@ impl AudioData {
sync_offset, sync_offset,
.. ..
} => Ok((chunks.clone(), *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, .. length: old_length, ..
} => { } => {
if buffer.len() != *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 }; *self = Self::Consolidated { buffer };
Ok(()) Ok(())
} }
_ => Err(LooperError::OutOfBounds(std::panic::Location::caller())), _ => Err(LooperError::AudioDataState(std::panic::Location::caller())),
} }
} }
@ -159,7 +159,7 @@ impl AudioData {
sync_offset: usize, sync_offset: usize,
length: usize, length: usize,
output_slice: &mut [f32], output_slice: &mut [f32],
mut logical_position: usize, logical_position: usize,
volume: f32, volume: f32,
) -> Result<()> { ) -> Result<()> {
if length == 0 { if length == 0 {
@ -167,15 +167,26 @@ impl AudioData {
return Ok(()); 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 mut samples_written = 0;
let samples_needed = output_slice.len(); let samples_needed = output_slice.len();
let mut current_logical_position = logical_position;
while samples_written < samples_needed { while samples_written < samples_needed {
// Map logical position to buffer position using sync offset // 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; 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 readable_samples_from_here = length - buffer_position;
let samples_to_copy = samples_remaining_in_output.min(readable_samples_from_here); let samples_to_copy = samples_remaining_in_output.min(readable_samples_from_here);
@ -192,7 +203,7 @@ impl AudioData {
} }
samples_written += samples_to_copy; samples_written += samples_to_copy;
logical_position = (logical_position + samples_to_copy) % length; current_logical_position = (current_logical_position + samples_to_copy) % length;
} }
Ok(()) Ok(())
@ -203,7 +214,7 @@ impl AudioData {
&self, &self,
buffer: &[f32], buffer: &[f32],
output_slice: &mut [f32], output_slice: &mut [f32],
mut logical_position: usize, logical_position: usize,
volume: f32, volume: f32,
) -> Result<()> { ) -> Result<()> {
let length = buffer.len(); let length = buffer.len();
@ -212,33 +223,25 @@ impl AudioData {
return Ok(()); return Ok(());
} }
let mut samples_written = 0; // Check if the logical position is beyond the available data
let samples_needed = output_slice.len(); if logical_position >= length {
return Err(LooperError::AudioDataState(std::panic::Location::caller()));
}
while samples_written < samples_needed { // Check if the requested samples would go beyond the available data
let samples_remaining = samples_needed - samples_written; if logical_position + output_slice.len() > length {
let samples_until_loop = length - logical_position; return Err(LooperError::AudioDataState(std::panic::Location::caller()));
let samples_to_copy = samples_remaining.min(samples_until_loop); }
// Direct copy since consolidated buffer is already reordered // Direct copy since consolidated buffer is already reordered
let src = &buffer[logical_position..logical_position + samples_to_copy]; let src = &buffer[logical_position..logical_position + output_slice.len()];
let dest = &mut output_slice[samples_written..samples_written + samples_to_copy]; output_slice.copy_from_slice(src);
dest.copy_from_slice(src);
// Apply volume scaling in-place // Apply volume scaling in-place
for sample in dest { for sample in output_slice {
*sample *= volume; *sample *= volume;
} }
samples_written += samples_to_copy;
logical_position += samples_to_copy;
// Handle looping
if logical_position >= length {
logical_position = 0;
}
}
Ok(()) Ok(())
} }
} }
@ -334,18 +337,15 @@ mod tests {
let sync_offset = 2; let sync_offset = 2;
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap(); 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) let samples = vec![10.0, 20.0, 30.0, 40.0, 50.0];
// 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
audio_data.append_samples(&samples, &mut factory).unwrap(); audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 4]; let mut output = vec![0.0; 5];
audio_data audio_data
.copy_samples_to_output(&mut output, 0, 1.0) .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![40.0, 50.0, 10.0, 20.0, 30.0]);
assert_eq!(output, vec![30.0, 40.0, 10.0, 20.0]);
} }
#[test] #[test]
@ -364,43 +364,6 @@ mod tests {
assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]); 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] #[test]
fn test_copy_samples_consolidated() { fn test_copy_samples_consolidated() {
// Create consolidated data directly // Create consolidated data directly
@ -443,7 +406,7 @@ mod tests {
#[test] #[test]
fn test_set_consolidated_buffer() { fn test_set_consolidated_buffer() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]); 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]; let samples = vec![1.0, 2.0, 3.0, 4.0];
audio_data.append_samples(&samples, &mut factory).unwrap(); audio_data.append_samples(&samples, &mut factory).unwrap();
@ -498,4 +461,55 @@ mod tests {
_ => panic!("Expected Empty variant after clear"), _ => 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, &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; *output_val += *scratch_pad_val;
} }
} }
@ -169,7 +173,8 @@ impl<const ROWS: usize> Column<ROWS> {
// Update playback position // Update playback position
if new_len > 0 { 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 { } else {
// During recording of first track, don't use modulo - let position grow // During recording of first track, don't use modulo - let position grow
// so that beat calculation works correctly // so that beat calculation works correctly

View File

@ -8,11 +8,11 @@ pub struct MatrixControllers<'a> {
} }
pub struct ColumnControllers<'a> { pub struct ColumnControllers<'a> {
post_record: &'a PostRecordController, pub(crate) post_record: &'a PostRecordController,
osc: &'a OscController, pub(crate) osc: &'a OscController,
persistence: &'a PersistenceManagerController, pub(crate) persistence: &'a PersistenceManagerController,
sample_rate: usize, pub(crate) sample_rate: usize,
column: usize, pub(crate) column: usize,
} }
pub struct TrackControllers<'a> { pub struct TrackControllers<'a> {

View File

@ -7,10 +7,7 @@ pub enum LooperError {
ChunkAllocation(&'static std::panic::Location<'static>), ChunkAllocation(&'static std::panic::Location<'static>),
#[error("Cannot modify chunk with multiple references")] #[error("Cannot modify chunk with multiple references")]
ChunkOwnership(&'static std::panic::Location<'static>), AudioDataState(&'static std::panic::Location<'static>),
#[error("Index out of bounds")]
OutOfBounds(&'static std::panic::Location<'static>),
#[error("Failed to send OSC message")] #[error("Failed to send OSC message")]
Osc(&'static std::panic::Location<'static>), Osc(&'static std::panic::Location<'static>),

View File

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

View File

@ -58,7 +58,8 @@ impl Metronome {
self.last_beat_time = Some(current_frame_time); self.last_beat_time = Some(current_frame_time);
// Check if there are multiple beats in the first buffer // 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; let buffer_end = current_frame_time + buffer_size as u32 - 1;
if next_beat_after_first <= buffer_end { if next_beat_after_first <= buffer_end {
panic!("Multiple beats in single buffer not supported"); 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) // Only continue if the time difference makes sense (not a huge wraparound)
// and we're still within the click duration // 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 click_output = audio_backend.click_output();
let start_offset = frames_since_beat as usize; 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 { 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 audio_output = [0.0; 50];
let mut click_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(); let timing = metronome.process(&mut backend, &osc).unwrap();
@ -366,17 +372,20 @@ mod tests {
let mut click_output = [0.0; 50]; let mut click_output = [0.0; 50];
// First beat at frame 0 // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(timing.beat_in_buffer, Some(0));
// No beat in next buffer (frames 50-99) // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, None); assert_eq!(timing.beat_in_buffer, None);
// Second beat at frame 100 (index 0 in buffer starting at frame 100) // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(timing.beat_in_buffer, Some(0));
} }
@ -390,7 +399,8 @@ mod tests {
let mut click_output = [0.0; 50]; let mut click_output = [0.0; 50];
// Beat occurs at frame 125, buffer starts at frame 120 // 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 // 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_frame_time = Some(119);
metronome.last_beat_time = Some(25); metronome.last_beat_time = Some(25);
@ -411,7 +421,8 @@ mod tests {
metronome.last_frame_time = Some(129); metronome.last_frame_time = Some(129);
metronome.last_beat_time = Some(100); 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, None); assert_eq!(timing.beat_in_buffer, None);
@ -427,7 +438,8 @@ mod tests {
let mut audio_output = [0.0; 200]; let mut audio_output = [0.0; 200];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
} }
@ -448,7 +460,8 @@ mod tests {
let mut audio_output = [0.0; 50]; let mut audio_output = [0.0; 50];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Should have click samples at the beginning // Should have click samples at the beginning
@ -472,7 +485,8 @@ mod tests {
let mut audio_output = [0.0; 50]; let mut audio_output = [0.0; 50];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Should have scaled click samples // Should have scaled click samples
@ -498,7 +512,8 @@ mod tests {
let mut audio_output = [0.0; 20]; let mut audio_output = [0.0; 20];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Click should start at index 5 (frame 105 - 100) // Click should start at index 5 (frame 105 - 100)
@ -528,7 +543,8 @@ mod tests {
let mut audio_output = [0.0; 10]; let mut audio_output = [0.0; 10];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Should have click starting at index 5 // Should have click starting at index 5
@ -536,7 +552,8 @@ mod tests {
assert_eq!(click_output[9], 0.8); assert_eq!(click_output[9], 0.8);
// Second buffer: continue click from previous buffer // 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(); metronome.process(&mut backend, &osc).unwrap();
// Should continue click from frame 5 onwards (index 0-9 should have click) // 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 audio_output = [0.0; 30];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// No click should be present // No click should be present
@ -581,7 +599,8 @@ mod tests {
let mut click_output = [0.0; 10]; let mut click_output = [0.0; 10];
// Jump from frame 50 to frame 150 (missed 90 frames) // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 90); // 150 - 50 - 10 = 90 assert_eq!(timing.missed_frames, 90); // 150 - 50 - 10 = 90
@ -601,7 +620,8 @@ mod tests {
let mut click_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) // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 120); // 180 - 50 - 10 = 120 assert_eq!(timing.missed_frames, 120); // 180 - 50 - 10 = 120
@ -621,7 +641,8 @@ mod tests {
let mut click_output = [0.0; 10]; let mut click_output = [0.0; 10];
// Jump from frame 25 to frame 200, missing multiple beats // 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()); assert!(metronome.process(&mut backend, &osc).is_err());
} }
@ -639,12 +660,14 @@ mod tests {
let mut click_output = [0.0; 10]; let mut click_output = [0.0; 10];
// First: xrun with missed beat // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert!(timing.beat_in_missed.is_some()); assert!(timing.beat_in_missed.is_some());
// Second: normal operation should continue correctly // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 0); assert_eq!(timing.missed_frames, 0);
assert_eq!(timing.beat_in_missed, None); assert_eq!(timing.beat_in_missed, None);
@ -661,7 +684,8 @@ mod tests {
let mut click_output = [0.0; 200]; let mut click_output = [0.0; 200];
// This should trigger the multiple beats panic // 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(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
metronome.process(&mut backend, &osc).unwrap(); metronome.process(&mut backend, &osc).unwrap();
})); }));
@ -676,7 +700,8 @@ mod tests {
let mut audio_output = [0.0; 1]; let mut audio_output = [0.0; 1];
let mut click_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(); let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0)); assert_eq!(timing.beat_in_buffer, Some(0));
@ -699,7 +724,8 @@ mod tests {
let mut audio_output = [0.0; 10]; let mut audio_output = [0.0; 10];
let mut click_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(); let timing = metronome.process(&mut backend, &osc).unwrap();
// Should behave as if starting fresh (first beat detection) // Should behave as if starting fresh (first beat detection)
@ -717,11 +743,13 @@ mod tests {
let mut audio_output = [0.0; 30]; let mut audio_output = [0.0; 30];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Filter only clock messages (0xF8) // Filter only clock messages (0xF8)
let clock_messages: Vec<_> = backend.midi_output_events let clock_messages: Vec<_> = backend
.midi_output_events
.iter() .iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xF8) .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xF8)
.collect(); .collect();
@ -744,11 +772,13 @@ mod tests {
let mut audio_output = [0.0; 50]; let mut audio_output = [0.0; 50];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Should have MIDI start message (0xFA) at the beginning // 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() .iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect(); .collect();
@ -764,11 +794,13 @@ mod tests {
let mut audio_output = [0.0; 50]; let mut audio_output = [0.0; 50];
let mut click_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(); metronome.process(&mut backend, &osc).unwrap();
// Should have SPP reset message (0xF2, 0x00, 0x00) at the beginning // 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() .iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect(); .collect();
@ -786,13 +818,15 @@ mod tests {
let mut click_output = [0.0; 20]; let mut click_output = [0.0; 20];
// First buffer (frames 0-19) // 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(); metronome.process(&mut backend, &osc).unwrap();
// Should have clocks at frames 0, 4, 8, 12, 16 // Should have clocks at frames 0, 4, 8, 12, 16
// Second buffer (frames 20-39) // 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(); metronome.process(&mut backend, &osc).unwrap();
// Should continue with clocks at frames 0, 4, 8, 12, 16 (relative to buffer start) // 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]; let mut click_output = [0.0; 10];
// Jump from frame 10 to frame 100 (xrun, missed frames 11-89) // 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(); metronome.process(&mut backend, &osc).unwrap();
// Should still output MIDI clocks for the current buffer // Should still output MIDI clocks for the current buffer
@ -840,10 +875,12 @@ mod tests {
let mut click_output = [0.0; 50]; let mut click_output = [0.0; 50];
// First beat - should have SPP reset // 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(); metronome.process(&mut backend, &osc).unwrap();
let spp_messages: Vec<_> = backend.midi_output_events let spp_messages: Vec<_> = backend
.midi_output_events
.iter() .iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect(); .collect();
@ -851,19 +888,24 @@ mod tests {
// Process several buffers to get to the next beat (200 frames total) // Process several buffers to get to the next beat (200 frames total)
// Frames 50-99 // 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(); metronome.process(&mut backend, &osc).unwrap();
// Frames 100-149 // 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(); metronome.process(&mut backend, &osc).unwrap();
// Frames 150-199 // 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(); metronome.process(&mut backend, &osc).unwrap();
// Frames 200-249 (contains next beat at frame 200) // 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(); metronome.process(&mut backend, &osc).unwrap();
let spp_messages: Vec<_> = backend.midi_output_events let spp_messages: Vec<_> = backend
.midi_output_events
.iter() .iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2) .filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect(); .collect();
@ -883,10 +925,12 @@ mod tests {
let mut click_output = [0.0; 50]; let mut click_output = [0.0; 50];
// First buffer should have Start message // 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(); metronome.process(&mut backend, &osc).unwrap();
let start_messages: Vec<_> = backend.midi_output_events let start_messages: Vec<_> = backend
.midi_output_events
.iter() .iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect(); .collect();
@ -896,14 +940,17 @@ mod tests {
metronome.set_frames_per_beat(200); metronome.set_frames_per_beat(200);
// Send tempo change messages manually for this test // 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(); 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() .iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFC) .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFC)
.collect(); .collect();
let start_messages: Vec<_> = backend.midi_output_events let start_messages: Vec<_> = backend
.midi_output_events
.iter() .iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA) .filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect(); .collect();
@ -925,7 +972,8 @@ mod tests {
let mut audio_output = [0.0; 50]; let mut audio_output = [0.0; 50];
let mut click_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(); let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have a beat and send OSC message // Should have a beat and send OSC message
@ -933,7 +981,10 @@ mod tests {
// Check OSC message was sent // Check OSC message was sent
let msg = osc_rx.try_recv().unwrap(); 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] #[test]
@ -945,7 +996,8 @@ mod tests {
let audio_input1 = [0.0; 30]; let audio_input1 = [0.0; 30];
let mut audio_output1 = [0.0; 30]; let mut audio_output1 = [0.0; 30];
let mut click_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(); metronome.process(&mut backend1, &osc).unwrap();
// Clear any messages from initialization // Clear any messages from initialization
@ -955,7 +1007,8 @@ mod tests {
let audio_input = [0.0; 30]; let audio_input = [0.0; 30];
let mut audio_output = [0.0; 30]; let mut audio_output = [0.0; 30];
let mut click_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(); let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have no beat (next beat would be at frame 100, buffer is 50-79) // 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]; let mut click_output = [0.0; 10];
// Jump to frame 180, missing frames 60-169 (next beat at frame 100 was missed) // 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(); let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have missed beat // Should have missed beat
@ -988,6 +1042,9 @@ mod tests {
// Check OSC message was sent for the missed beat // Check OSC message was sent for the missed beat
let msg = osc_rx.try_recv().unwrap(); 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::*; use crate::*;
/// Process MIDI events /// 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, audio_backend: &mut A,
process_handler: &mut ProcessHandler<F, COLS, ROWS>, process_handler: &mut ProcessHandler<F, COLS, ROWS>,
) -> Result<()> { ) -> 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]; raw_events[event_count][2] = midi_event.bytes[2];
event_count += 1; event_count += 1;
} else { } 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)?; .process(audio_backend, &timing, &controllers)?;
for i in 0..audio_backend.audio_output().len() { 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(()) Ok(())
@ -247,9 +248,7 @@ mod tests {
const BUFFER_SIZE: usize = 10; const BUFFER_SIZE: usize = 10;
// Setup // Setup
let chunk_factory = MockFactory::new( let chunk_factory = MockFactory::new((0..50).map(|_| AudioChunk::allocate(1024)).collect());
(0..50).map(|_| AudioChunk::allocate(1024)).collect()
);
let beep = Arc::new(AudioChunk { let beep = Arc::new(AudioChunk {
samples: vec![1.0; 1].into_boxed_slice(), samples: vec![1.0; 1].into_boxed_slice(),
@ -265,7 +264,8 @@ mod tests {
..Default::default() ..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_tx, _osc_rx) = kanal::bounded(64);
let osc = OscController::new(osc_tx); let osc = OscController::new(osc_tx);
@ -282,12 +282,12 @@ mod tests {
post_record_controller, post_record_controller,
osc, osc,
persistence, persistence,
).unwrap(); )
.unwrap();
let audio_output_buffer = &mut [0.0; BUFFER_SIZE]; let audio_output_buffer = &mut [0.0; BUFFER_SIZE];
let click_output_buffer = &mut [0.0; BUFFER_SIZE]; let click_output_buffer = &mut [0.0; BUFFER_SIZE];
// Warm up, no commands here // Warm up, no commands here
let mut last_frame_time = 0; let mut last_frame_time = 0;
let input = [last_frame_time as f32; BUFFER_SIZE]; let input = [last_frame_time as f32; BUFFER_SIZE];
@ -301,12 +301,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); 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_column, 0);
assert_eq!(handler.selected_row, 0); assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Press record
last_frame_time = 10; last_frame_time = 10;
@ -321,12 +335,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); 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_column, 0);
assert_eq!(handler.selected_row, 0); assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) starts
last_frame_time = 20; last_frame_time = 20;
@ -341,12 +369,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) first beat continues
last_frame_time = 30; last_frame_time = 30;
@ -361,12 +403,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) begin second beat
last_frame_time = 40; last_frame_time = 40;
@ -381,12 +437,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) second beat continues
last_frame_time = 50; last_frame_time = 50;
@ -401,12 +471,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) second beat continues
last_frame_time = 60; last_frame_time = 60;
@ -421,12 +505,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Record track (0, 0) 3rd beat starts
last_frame_time = 70; last_frame_time = 70;
@ -441,12 +539,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Press record end
last_frame_time = 80; last_frame_time = 80;
@ -461,12 +573,26 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); 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_column, 0);
assert_eq!(handler.selected_row, 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!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Track (0, 0) stops recording, starts playing first beat
last_frame_time = 90; last_frame_time = 90;
@ -481,13 +607,32 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
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"); audio_output_buffer
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); .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_column, 0);
assert_eq!(handler.selected_row, 0); assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Press down
last_frame_time = 100; last_frame_time = 100;
@ -502,13 +647,32 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
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"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Press record
last_frame_time = 110; last_frame_time = 110;
@ -523,13 +687,32 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
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"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty); 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 // Recording track (0, 1) starts recording at second column beat
last_frame_time = 120; last_frame_time = 120;
@ -544,13 +727,36 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Recording track (0, 1) continues recording
last_frame_time = 130; last_frame_time = 130;
@ -565,13 +771,36 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Track (0, 1) records for second beat, column is at 3rd beat
last_frame_time = 140; last_frame_time = 140;
@ -586,13 +815,36 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Column beat 3 continues
last_frame_time = 150; last_frame_time = 150;
@ -607,13 +859,36 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Column wraps to first beat, track (0, 1) records 3rd beat
last_frame_time = 160; last_frame_time = 160;
@ -628,13 +903,36 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Continue playback and recording
last_frame_time = 170; last_frame_time = 170;
@ -649,13 +947,30 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Continue playback and recording
last_frame_time = 180; last_frame_time = 180;
@ -670,13 +985,30 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 }); 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 // Recording track (0, 1) auto stops, mixed playback starts
last_frame_time = 190; last_frame_time = 190;
@ -691,17 +1023,37 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
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)"); audio_output_buffer
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)"); .iter()
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing); 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 // 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:#?}"); assert!(prh_result.is_err(), "Expected timeout, got {prh_result:#?}");
// Playback consolidated // Playback consolidated
@ -717,12 +1069,25 @@ mod tests {
midi_output_events: vec![], midi_output_events: vec![],
}; };
handler.process(&mut backend).unwrap(); 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!(
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)"); audio_output_buffer
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click"); .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_column, 0);
assert_eq!(handler.selected_row, 1); assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing); assert_eq!(
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing); 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 // Process samples after beat with new current state
if (beat_index_in_buffer as usize) < output_buffer.len() { if (beat_index_in_buffer as usize) < output_buffer.len() {
// Calculate position after beat for remaining samples // Calculate position after beat for remaining samples
let mut post_beat_position = playback_position + beat_index_in_buffer as usize; let post_beat_position = playback_position + beat_index_in_buffer as usize;
if self.audio_data.len() > 0 {
post_beat_position %= self.audio_data.len();
}
self.process_audio_range( self.process_audio_range(
&audio_backend.audio_input()[beat_index_in_buffer as _..], &audio_backend.audio_input()[beat_index_in_buffer as _..],
@ -244,11 +241,40 @@ impl Track {
output_buffer.fill(0.0); output_buffer.fill(0.0);
} }
TrackState::Playing => { 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( self.audio_data.copy_samples_to_output(
output_buffer, output_buffer,
playback_position, wrapped_position,
self.volume, 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);
}
} }
} }