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)]
impl Default for Args {
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 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()
}
@ -96,4 +96,4 @@ impl<'a> AudioBackend<'a> for MockAudioBackend<'a> {
self.midi_output_events.push((time, bytes.to_vec()));
Ok(())
}
}
}

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,31 +223,23 @@ impl AudioData {
return Ok(());
}
let mut samples_written = 0;
let samples_needed = output_slice.len();
// Check if the logical position is beyond the available data
if logical_position >= length {
return Err(LooperError::AudioDataState(std::panic::Location::caller()));
}
while samples_written < samples_needed {
let samples_remaining = samples_needed - samples_written;
let samples_until_loop = length - logical_position;
let samples_to_copy = samples_remaining.min(samples_until_loop);
// Check if the requested samples would go beyond the available data
if logical_position + output_slice.len() > length {
return Err(LooperError::AudioDataState(std::panic::Location::caller()));
}
// Direct copy since consolidated buffer is already reordered
let src = &buffer[logical_position..logical_position + samples_to_copy];
let dest = &mut output_slice[samples_written..samples_written + samples_to_copy];
dest.copy_from_slice(src);
// Direct copy since consolidated buffer is already reordered
let src = &buffer[logical_position..logical_position + output_slice.len()];
output_slice.copy_from_slice(src);
// Apply volume scaling in-place
for sample in dest {
*sample *= volume;
}
samples_written += samples_to_copy;
logical_position += samples_to_copy;
// Handle looping
if logical_position >= length {
logical_position = 0;
}
// Apply volume scaling in-place
for sample in output_slice {
*sample *= volume;
}
Ok(())
@ -334,18 +337,15 @@ mod tests {
let sync_offset = 2;
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
// Recording order: [A, B, C, D] but recorded starting at beat 3 (sync_offset=2)
// So logical mapping should be: beat1=C, beat2=D, beat3=A, beat4=B
let samples = vec![10.0, 20.0, 30.0, 40.0]; // A, B, C, D
let samples = vec![10.0, 20.0, 30.0, 40.0, 50.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 4];
let mut output = vec![0.0; 5];
audio_data
.copy_samples_to_output(&mut output, 0, 1.0)
.unwrap(); // Start at logical beat 1
.unwrap();
// Should get: [C, D, A, B] = [30.0, 40.0, 10.0, 20.0]
assert_eq!(output, vec![30.0, 40.0, 10.0, 20.0]);
assert_eq!(output, vec![40.0, 50.0, 10.0, 20.0, 30.0]);
}
#[test]
@ -364,43 +364,6 @@ mod tests {
assert_eq!(output, vec![0.5, 1.0, 1.5, 2.0]);
}
#[test]
fn test_copy_samples_unconsolidated_looping() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
let samples = vec![1.0, 2.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 6]; // Request more samples than available
audio_data
.copy_samples_to_output(&mut output, 0, 1.0)
.unwrap();
// Should loop: [1.0, 2.0, 1.0, 2.0, 1.0, 2.0]
assert_eq!(output, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
}
#[test]
fn test_copy_samples_unconsolidated_offset_with_looping() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
let sync_offset = 1;
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
// Recording: [A, B, C] starting at beat 2 (sync_offset=1)
// Logical mapping: beat1=B, beat2=C, beat3=A
let samples = vec![10.0, 20.0, 30.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 6]; // Request 2 full loops
audio_data
.copy_samples_to_output(&mut output, 0, 1.0)
.unwrap();
// Should get: [B, C, A, B, C, A] = [20.0, 30.0, 10.0, 20.0, 30.0, 10.0]
assert_eq!(output, vec![20.0, 30.0, 10.0, 20.0, 30.0, 10.0]);
}
#[test]
fn test_copy_samples_consolidated() {
// Create consolidated data directly
@ -443,7 +406,7 @@ mod tests {
#[test]
fn test_set_consolidated_buffer() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 100).unwrap();
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
let samples = vec![1.0, 2.0, 3.0, 4.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
@ -498,4 +461,55 @@ mod tests {
_ => panic!("Expected Empty variant after clear"),
}
}
#[test]
fn test_copy_samples_unconsolidated_overflow() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
let mut audio_data = AudioData::new_unconsolidated(&mut factory, 0).unwrap();
let samples = vec![1.0, 2.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 3];
let res = audio_data.copy_samples_to_output(&mut output, 0, 1.0);
assert!(res.is_err());
}
#[test]
fn test_copy_samples_unconsolidated_overflow_offset() {
let mut factory = MockFactory::new(vec![AudioChunk::allocate(10)]);
let sync_offset = 1;
let mut audio_data = AudioData::new_unconsolidated(&mut factory, sync_offset).unwrap();
let samples = vec![10.0, 20.0];
audio_data.append_samples(&samples, &mut factory).unwrap();
let mut output = vec![0.0; 2];
let res = audio_data.copy_samples_to_output(&mut output, 1, 1.0);
assert!(res.is_err());
}
#[test]
fn test_copy_samples_consolidated_overflow() {
let buffer = vec![10.0, 20.0].into_boxed_slice();
let audio_data = AudioData::Consolidated { buffer };
let mut output = vec![0.0; 3];
let res = audio_data.copy_samples_to_output(&mut output, 0, 1.0);
assert!(res.is_err());
}
#[test]
fn test_copy_samples_consolidated_overflow_offset() {
let buffer = vec![10.0, 20.0].into_boxed_slice();
let audio_data = AudioData::Consolidated { buffer };
let mut output = vec![0.0; 2];
let res = audio_data.copy_samples_to_output(&mut output, 1, 1.0);
assert!(res.is_err());
}
}

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");
@ -217,4 +217,4 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> jack::ProcessHandler
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
}
}
*/
*/

View File

@ -42,7 +42,7 @@ impl Metronome {
) -> Result<BufferTiming> {
let current_frame_time = audio_backend.last_frame_time();
let buffer_size = audio_backend.n_frames() as usize;
// Clear the click output buffer first
let click_output = audio_backend.click_output();
for sample in click_output.iter_mut() {
@ -51,39 +51,40 @@ impl Metronome {
// Output MIDI messages
self.output_midi_messages(audio_backend, current_frame_time, buffer_size)?;
// Handle first-time initialization
if self.last_frame_time.is_none() {
self.last_frame_time = Some(current_frame_time);
self.last_beat_time = Some(current_frame_time);
// Check if there are multiple beats in the first buffer
let next_beat_after_first = current_frame_time.wrapping_add(self.frames_per_beat as u32);
let next_beat_after_first =
current_frame_time.wrapping_add(self.frames_per_beat as u32);
let buffer_end = current_frame_time + buffer_size as u32 - 1;
if next_beat_after_first <= buffer_end {
panic!("Multiple beats in single buffer not supported");
}
// First beat occurs at the start of the first buffer
let timing = BufferTiming {
beat_in_buffer: Some(0),
missed_frames: 0,
beat_in_missed: None,
};
// Play click and send OSC message
self.play_click_at_position(audio_backend, 0)?;
osc_controller.metronome_position_changed(0.0)?;
// Update state
self.last_frame_time = Some(current_frame_time + buffer_size as u32 - 1);
return Ok(timing);
}
let last_frame = self.last_frame_time.unwrap();
let expected_frame = last_frame + 1;
// Check for xrun (audio discontinuity)
let missed_frames = if current_frame_time != expected_frame {
if current_frame_time > expected_frame {
@ -99,32 +100,32 @@ impl Metronome {
} else {
0
};
let mut beat_in_buffer = None;
let mut beat_in_missed = None;
if missed_frames > 0 {
// Handle xrun case - missed range excludes current buffer
let missed_start = last_frame + 1;
let missed_end = current_frame_time - 1;
// Check if a beat occurred in the missed frames
if let Some(last_beat) = self.last_beat_time {
let next_beat_frame = last_beat.wrapping_add(self.frames_per_beat as u32);
if next_beat_frame >= missed_start && next_beat_frame <= missed_end {
// Verify only one beat was missed
let beat_after_next = next_beat_frame.wrapping_add(self.frames_per_beat as u32);
if beat_after_next <= missed_end {
return Err(LooperError::Xrun(std::panic::Location::caller()));
}
beat_in_missed = Some((next_beat_frame - missed_start) as u32);
self.last_beat_time = Some(next_beat_frame);
// Send OSC message for missed beat
osc_controller.metronome_position_changed(0.0)?;
// Send SPP update for missed beat
let beats_since_start = next_beat_frame / self.frames_per_beat as u32;
let lsb = (beats_since_start & 0x7F) as u8;
@ -133,46 +134,46 @@ impl Metronome {
}
}
}
// Check for beat in current buffer
if let Some(last_beat) = self.last_beat_time {
let next_beat_frame = last_beat.wrapping_add(self.frames_per_beat as u32);
let buffer_start = current_frame_time;
let buffer_end = current_frame_time + buffer_size as u32 - 1;
if next_beat_frame >= buffer_start && next_beat_frame <= buffer_end {
// Verify only one beat in buffer
let beat_after_next = next_beat_frame.wrapping_add(self.frames_per_beat as u32);
if beat_after_next <= buffer_end {
panic!("Multiple beats in single buffer not supported");
}
let beat_position = (next_beat_frame - buffer_start) as u32;
beat_in_buffer = Some(beat_position);
self.last_beat_time = Some(next_beat_frame);
// Play click and send OSC message
self.play_click_at_position(audio_backend, beat_position as usize)?;
osc_controller.metronome_position_changed(0.0)?;
// Send SPP update - calculate beats since start (first beat was at frame 0 on first buffer)
// Send SPP update - calculate beats since start (first beat was at frame 0 on first buffer)
let beats_since_start = next_beat_frame / self.frames_per_beat as u32;
let lsb = (beats_since_start & 0x7F) as u8;
let msb = ((beats_since_start >> 7) & 0x7F) as u8;
audio_backend.midi_output(beat_position, &[0xF2, lsb, msb])?;
}
}
// Always check if we need to continue a click from previous buffer,
// Always check if we need to continue a click from previous buffer,
// unless we just started a new beat in this buffer
if beat_in_buffer.is_none() {
self.continue_click_playback(audio_backend, current_frame_time)?;
}
// Update frame time tracking
self.last_frame_time = Some(current_frame_time + buffer_size as u32 - 1);
Ok(BufferTiming {
beat_in_buffer,
missed_frames,
@ -204,7 +205,7 @@ impl Metronome {
) -> Result<()> {
// Send Stop then Start on tempo change
audio_backend.midi_output(0, &[0xFC])?; // Stop
audio_backend.midi_output(0, &[0xFA])?; // Start
audio_backend.midi_output(0, &[0xFA])?; // Start
audio_backend.midi_output(0, &[0xF2, 0x00, 0x00])?; // SPP reset
Ok(())
}
@ -216,17 +217,17 @@ impl Metronome {
buffer_size: usize,
) -> Result<()> {
let frames_per_midi_clock = self.frames_per_beat / 24;
// Send Start message on first process call
if self.last_frame_time.is_none() {
audio_backend.midi_output(0, &[0xFA])?; // Start
audio_backend.midi_output(0, &[0xF2, 0x00, 0x00])?; // SPP reset
}
// Calculate MIDI clock positions in current buffer
let buffer_start = current_frame_time;
let buffer_end = current_frame_time + buffer_size as u32 - 1;
// Generate clocks for this buffer
// Start from the first clock boundary at or after buffer_start
let first_clock_in_buffer = if buffer_start % frames_per_midi_clock as u32 == 0 {
@ -234,7 +235,7 @@ impl Metronome {
} else {
((buffer_start / frames_per_midi_clock as u32) + 1) * frames_per_midi_clock as u32
};
let mut clock_frame = first_clock_in_buffer;
loop {
if clock_frame <= buffer_end {
@ -245,10 +246,10 @@ impl Metronome {
break;
}
}
// SPP will be updated when beats are detected in the main process logic
// This is handled after beat detection in the process method
Ok(())
}
@ -259,11 +260,11 @@ impl Metronome {
) -> Result<()> {
let click_output = audio_backend.click_output();
let samples_to_write = (self.click_samples.sample_count).min(click_output.len() - position);
for i in 0..samples_to_write {
click_output[position + i] = self.click_samples.samples[i] * self.click_volume;
}
Ok(())
}
@ -275,20 +276,24 @@ impl Metronome {
// Check if we should continue a click from the previous buffer
if let Some(last_beat) = self.last_beat_time {
let frames_since_beat = current_frame_time.wrapping_sub(last_beat);
// Only continue if the time difference makes sense (not a huge wraparound)
// and we're still within the click duration
if frames_since_beat < self.click_samples.sample_count as u32 && frames_since_beat < 1000000 {
if frames_since_beat < self.click_samples.sample_count as u32
&& frames_since_beat < 1000000
{
let click_output = audio_backend.click_output();
let start_offset = frames_since_beat as usize;
let samples_to_write = (self.click_samples.sample_count - start_offset).min(click_output.len());
let samples_to_write =
(self.click_samples.sample_count - start_offset).min(click_output.len());
for i in 0..samples_to_write {
click_output[i] += self.click_samples.samples[start_offset + i] * self.click_volume;
click_output[i] +=
self.click_samples.samples[start_offset + i] * self.click_volume;
}
}
}
Ok(())
}
}
@ -347,11 +352,12 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0));
assert_eq!(timing.missed_frames, 0);
assert_eq!(timing.beat_in_missed, None);
@ -364,19 +370,22 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
// First beat at frame 0
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0));
// No beat in next buffer (frames 50-99)
let mut backend = create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, None);
// Second beat at frame 100 (index 0 in buffer starting at frame 100)
let mut backend = create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0));
}
@ -388,13 +397,14 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
// Beat occurs at frame 125, buffer starts at frame 120
let mut backend = create_mock_audio_backend(50, 120, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 120, &audio_input, &mut audio_output, &mut click_output);
// Set up state where last beat was at frame 25, so next beat is at 25 + 100 = 125
metronome.last_frame_time = Some(119);
metronome.last_beat_time = Some(25);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(5)); // Frame 125 - 120 = index 5
}
@ -406,14 +416,15 @@ mod tests {
let audio_input = [0.0; 30];
let mut audio_output = [0.0; 30];
let mut click_output = [0.0; 30];
// Set up state where last beat was at frame 100, current buffer is frames 130-159
metronome.last_frame_time = Some(129);
metronome.last_beat_time = Some(100);
let mut backend = create_mock_audio_backend(30, 130, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(30, 130, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, None);
assert_eq!(timing.missed_frames, 0);
}
@ -426,8 +437,9 @@ mod tests {
let audio_input = [0.0; 200]; // Large buffer
let mut audio_output = [0.0; 200];
let mut click_output = [0.0; 200];
let mut backend = create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
}
@ -443,14 +455,15 @@ mod tests {
};
let mut metronome = Metronome::new(click_samples, &state);
let (osc, _osc_rx) = create_osc_controller();
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have click samples at the beginning
assert_eq!(click_output[0], 0.8);
assert_eq!(click_output[9], 0.8);
@ -467,14 +480,15 @@ mod tests {
};
let mut metronome = Metronome::new(click_samples, &state);
let (osc, _osc_rx) = create_osc_controller();
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have scaled click samples
assert_eq!(click_output[0], 0.4); // 0.8 * 0.5
}
@ -489,18 +503,19 @@ mod tests {
};
let mut metronome = Metronome::new(click_samples, &state);
let (osc, _osc_rx) = create_osc_controller();
// Set up for beat at frame 105, buffer starts at 100
metronome.last_frame_time = Some(99);
metronome.last_beat_time = Some(5);
let audio_input = [0.0; 20];
let mut audio_output = [0.0; 20];
let mut click_output = [0.0; 20];
let mut backend = create_mock_audio_backend(20, 100, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(20, 100, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Click should start at index 5 (frame 105 - 100)
assert_eq!(click_output[4], 0.0);
assert_eq!(click_output[5], 0.8);
@ -519,26 +534,28 @@ mod tests {
};
let mut metronome = Metronome::new(click_samples, &state);
let (osc, _osc_rx) = create_osc_controller();
// First buffer: beat starts at frame 95 (index 5 in buffer 90-99)
metronome.last_frame_time = Some(89);
metronome.last_beat_time = Some(95u32.wrapping_sub(100)); // Next beat = this + 100 = 95
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
let mut backend = create_mock_audio_backend(10, 90, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 90, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have click starting at index 5
assert_eq!(click_output[5], 0.8);
assert_eq!(click_output[9], 0.8);
// Second buffer: continue click from previous buffer
let mut backend = create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should continue click from frame 5 onwards (index 0-9 should have click)
assert_eq!(click_output[0], 0.8);
assert_eq!(click_output[9], 0.8);
@ -548,19 +565,20 @@ mod tests {
fn test_no_click_when_no_beat() {
let mut metronome = create_test_metronome(100);
let (osc, _osc_rx) = create_osc_controller();
// Set up state where no beat occurs in this buffer and last click has finished
// Last beat was 150 frames ago, so click (which is 100 frames) has finished
metronome.last_frame_time = Some(49);
metronome.last_beat_time = Some(50u32.wrapping_sub(150));
let audio_input = [0.0; 30];
let mut audio_output = [0.0; 30];
let mut click_output = [0.0; 30];
let mut backend = create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// No click should be present
assert!(click_output.iter().all(|&x| x == 0.0));
}
@ -571,19 +589,20 @@ mod tests {
fn test_xrun_detection() {
let mut metronome = create_test_metronome(100);
let (osc, _osc_rx) = create_osc_controller();
// Set up normal operation first
metronome.last_frame_time = Some(50);
metronome.last_beat_time = Some(0);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// Jump from frame 50 to frame 150 (missed 90 frames)
let mut backend = create_mock_audio_backend(10, 150, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 150, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 90); // 150 - 50 - 10 = 90
}
@ -591,19 +610,20 @@ mod tests {
fn test_beat_in_missed_frames() {
let mut metronome = create_test_metronome(100);
let (osc, _osc_rx) = create_osc_controller();
// Set up: last frame was 50, last beat was at 0
metronome.last_frame_time = Some(50);
metronome.last_beat_time = Some(0);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// Jump to frame 180, missing frames 60-169 (next beat at frame 100 was missed)
let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 120); // 180 - 50 - 10 = 120
assert_eq!(timing.beat_in_missed, Some(49)); // Beat at frame 100, missed range starts at 51, so 100-51=49
}
@ -612,16 +632,17 @@ mod tests {
fn test_multiple_beats_in_xrun_panics() {
let mut metronome = create_test_metronome(50); // Fast tempo
let (osc, _osc_rx) = create_osc_controller();
metronome.last_frame_time = Some(25);
metronome.last_beat_time = Some(0);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// Jump from frame 25 to frame 200, missing multiple beats
let mut backend = create_mock_audio_backend(10, 200, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 200, &audio_input, &mut audio_output, &mut click_output);
assert!(metronome.process(&mut backend, &osc).is_err());
}
@ -629,22 +650,24 @@ mod tests {
fn test_recovery_after_xrun() {
let mut metronome = create_test_metronome(100);
let (osc, _osc_rx) = create_osc_controller();
// Set up xrun scenario
metronome.last_frame_time = Some(50);
metronome.last_beat_time = Some(0);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// First: xrun with missed beat
let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert!(timing.beat_in_missed.is_some());
// Second: normal operation should continue correctly
let mut backend = create_mock_audio_backend(10, 190, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 190, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.missed_frames, 0);
assert_eq!(timing.beat_in_missed, None);
@ -659,9 +682,10 @@ mod tests {
let audio_input = [0.0; 200]; // Buffer larger than frames_per_beat
let mut audio_output = [0.0; 200];
let mut click_output = [0.0; 200];
// This should trigger the multiple beats panic
let mut backend = create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(200, 0, &audio_input, &mut audio_output, &mut click_output);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
metronome.process(&mut backend, &osc).unwrap();
}));
@ -675,10 +699,11 @@ mod tests {
let audio_input = [0.0; 1];
let mut audio_output = [0.0; 1];
let mut click_output = [0.0; 1];
let mut backend = create_mock_audio_backend(1, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(1, 0, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
assert_eq!(timing.beat_in_buffer, Some(0));
assert_eq!(click_output[0], 0.4); // Should have click with default volume (0.8 * 0.5)
}
@ -687,21 +712,22 @@ mod tests {
fn test_frames_per_beat_change_resets_metronome() {
let mut metronome = create_test_metronome(100);
let (osc, _osc_rx) = create_osc_controller();
// Set up some state
metronome.last_frame_time = Some(50);
metronome.last_beat_time = Some(25);
// Change frames per beat (simulating tempo change when no tracks playing)
metronome.set_frames_per_beat(200);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
let mut backend = create_mock_audio_backend(10, 60, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 60, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
// Should behave as if starting fresh (first beat detection)
assert_eq!(timing.beat_in_buffer, Some(0));
assert_eq!(timing.missed_frames, 0);
@ -716,21 +742,23 @@ mod tests {
let audio_input = [0.0; 30];
let mut audio_output = [0.0; 30];
let mut click_output = [0.0; 30];
let mut backend = create_mock_audio_backend(30, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(30, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Filter only clock messages (0xF8)
let clock_messages: Vec<_> = backend.midi_output_events
let clock_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xF8)
.collect();
// Should have MIDI clock messages at 24 PPQN
// 120 frames per beat / 24 = 5 frames per clock
// In a 30-frame buffer, we should have 6 clock messages (at frames 0, 5, 10, 15, 20, 25)
assert_eq!(clock_messages.len(), 6);
// Check timing - should be at 5-frame intervals
let times: Vec<u32> = clock_messages.iter().map(|(t, _)| *t).collect();
assert_eq!(times, vec![0, 5, 10, 15, 20, 25]);
@ -743,12 +771,14 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have MIDI start message (0xFA) at the beginning
let start_messages: Vec<_> = backend.midi_output_events
let start_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect();
@ -763,12 +793,14 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have SPP reset message (0xF2, 0x00, 0x00) at the beginning
let spp_messages: Vec<_> = backend.midi_output_events
let spp_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect();
@ -784,21 +816,23 @@ mod tests {
let audio_input = [0.0; 20];
let mut audio_output = [0.0; 20];
let mut click_output = [0.0; 20];
// First buffer (frames 0-19)
let mut backend = create_mock_audio_backend(20, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(20, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should have clocks at frames 0, 4, 8, 12, 16
// Second buffer (frames 20-39)
let mut backend = create_mock_audio_backend(20, 20, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(20, 20, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should continue with clocks at frames 0, 4, 8, 12, 16 (relative to buffer start)
// Which are absolute frames 20, 24, 28, 32, 36
assert!(backend.midi_output_events.len() > 0);
// Verify timing continuity
let times: Vec<u32> = backend.midi_output_events.iter().map(|(t, _)| *t).collect();
assert_eq!(times, vec![0, 4, 8, 12, 16]); // Relative to buffer start frame 20
@ -806,23 +840,24 @@ mod tests {
#[test]
fn test_midi_clock_handles_xrun() {
let mut metronome = create_test_metronome(48); // 48 frames per beat = 2 frames per clock
let mut metronome = create_test_metronome(48); // 48 frames per beat = 2 frames per clock
let (osc, _osc_rx) = create_osc_controller();
// Set up normal operation first
metronome.last_frame_time = Some(10);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// Jump from frame 10 to frame 100 (xrun, missed frames 11-89)
let mut backend = create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 100, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Should still output MIDI clocks for the current buffer
assert!(backend.midi_output_events.len() > 0);
// All clock messages should be for frames within the current buffer (100-109)
for (time, bytes) in &backend.midi_output_events {
if bytes.len() == 1 && bytes[0] == 0xF8 {
@ -838,32 +873,39 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
// First beat - should have SPP reset
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
let spp_messages: Vec<_> = backend.midi_output_events
let spp_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect();
assert_eq!(spp_messages[0].1, vec![0xF2, 0x00, 0x00]);
// Process several buffers to get to the next beat (200 frames total)
// Frames 50-99
let mut backend = create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 50, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Frames 100-149
let mut backend = create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output);
// Frames 100-149
let mut backend =
create_mock_audio_backend(50, 100, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Frames 150-199
let mut backend = create_mock_audio_backend(50, 150, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 150, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
// Frames 200-249 (contains next beat at frame 200)
let mut backend = create_mock_audio_backend(50, 200, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 200, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
let spp_messages: Vec<_> = backend.midi_output_events
let spp_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 3 && bytes[0] == 0xF2)
.collect();
@ -881,33 +923,38 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
// First buffer should have Start message
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
metronome.process(&mut backend, &osc).unwrap();
let start_messages: Vec<_> = backend.midi_output_events
let start_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect();
assert_eq!(start_messages.len(), 1);
// Change tempo - this resets the metronome state
metronome.set_frames_per_beat(200);
// Send tempo change messages manually for this test
let mut backend = create_mock_audio_backend(50, 60, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 60, &audio_input, &mut audio_output, &mut click_output);
metronome.send_tempo_change_messages(&mut backend).unwrap();
let stop_messages: Vec<_> = backend.midi_output_events
let stop_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFC)
.collect();
let start_messages: Vec<_> = backend.midi_output_events
let start_messages: Vec<_> = backend
.midi_output_events
.iter()
.filter(|(_, bytes)| bytes.len() == 1 && bytes[0] == 0xFA)
.collect();
assert_eq!(stop_messages.len(), 1);
assert_eq!(start_messages.len(), 1);
// Stop should come before Start (both at time 0)
@ -924,43 +971,49 @@ mod tests {
let audio_input = [0.0; 50];
let mut audio_output = [0.0; 50];
let mut click_output = [0.0; 50];
let mut backend = create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(50, 0, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have a beat and send OSC message
assert_eq!(timing.beat_in_buffer, Some(0));
// Check OSC message was sent
let msg = osc_rx.try_recv().unwrap();
assert!(matches!(msg, Some(osc::Message::MetronomePosition { position: 0.0 })));
assert!(matches!(
msg,
Some(osc::Message::MetronomePosition { position: 0.0 })
));
}
#[test]
fn test_osc_message_not_sent_when_no_beat() {
let mut metronome = create_test_metronome(100);
let (osc, osc_rx) = create_osc_controller();
// First, process one buffer normally to initialize properly
let audio_input1 = [0.0; 30];
let mut audio_output1 = [0.0; 30];
let mut click_output1 = [0.0; 30];
let mut backend1 = create_mock_audio_backend(30, 0, &audio_input1, &mut audio_output1, &mut click_output1);
let mut backend1 =
create_mock_audio_backend(30, 0, &audio_input1, &mut audio_output1, &mut click_output1);
metronome.process(&mut backend1, &osc).unwrap();
// Clear any messages from initialization
let _ = osc_rx.try_recv();
// Now process a buffer where no beat occurs
let audio_input = [0.0; 30];
let mut audio_output = [0.0; 30];
let mut click_output = [0.0; 30];
let mut backend = create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(30, 50, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have no beat (next beat would be at frame 100, buffer is 50-79)
assert_eq!(timing.beat_in_buffer, None);
// Check that the expected behavior occurred (may have OSC messages from xrun detection)
// The core test is that no beat was detected in the buffer
// OSC behavior for edge cases can vary based on implementation details
@ -970,24 +1023,28 @@ mod tests {
fn test_osc_message_sent_on_xrun_beat() {
let mut metronome = create_test_metronome(100);
let (osc, osc_rx) = create_osc_controller();
// Set up: last frame was 50, last beat was at 0
metronome.last_frame_time = Some(50);
metronome.last_beat_time = Some(0);
let audio_input = [0.0; 10];
let mut audio_output = [0.0; 10];
let mut click_output = [0.0; 10];
// Jump to frame 180, missing frames 60-169 (next beat at frame 100 was missed)
let mut backend = create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let mut backend =
create_mock_audio_backend(10, 180, &audio_input, &mut audio_output, &mut click_output);
let timing = metronome.process(&mut backend, &osc).unwrap();
// Should have missed beat
assert!(timing.beat_in_missed.is_some());
// Check OSC message was sent for the missed beat
let msg = osc_rx.try_recv().unwrap();
assert!(matches!(msg, Some(osc::Message::MetronomePosition { position: 0.0 })));
assert!(matches!(
msg,
Some(osc::Message::MetronomePosition { position: 0.0 })
));
}
}

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(())
@ -225,7 +226,7 @@ mod tests {
/// 30 Record track (0, 0) first beat continues
/// 40 48 Record track (0, 0) begin second beat
/// 50 Record track (0, 0) second beat continues
/// 60 Record track (0, 0) second beat continues
/// 60 Record track (0, 0) second beat continues
/// 70 72 Record track (0, 0) 3rd beat starts
/// 80 Press record end
/// 90 96 Track (0, 0) stops recording, starts playing first beat
@ -245,18 +246,16 @@ mod tests {
async fn test_recording_autostop_sync() {
const FRAMES_PER_BEAT: usize = 24;
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(),
sample_count: 1,
next: None,
});
let state = State {
metronome: MetronomeState {
frames_per_beat: FRAMES_PER_BEAT,
@ -265,14 +264,15 @@ 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);
let (pers_tx, _pers_rx) = kanal::bounded(64);
let persistence = PersistenceManagerController { sender: pers_tx };
let mut handler = ProcessHandler::<_, 5, 5>::new(
48000,
BUFFER_SIZE,
@ -282,12 +282,12 @@ mod tests {
post_record_controller,
osc,
persistence,
).unwrap();
)
.unwrap();
let audio_output_buffer = &mut [0.0; BUFFER_SIZE];
let click_output_buffer = &mut [0.0; BUFFER_SIZE];
// Warm up, no commands here
let mut last_frame_time = 0;
let input = [last_frame_time as f32; BUFFER_SIZE];
@ -301,12 +301,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Empty
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Press record
last_frame_time = 10;
@ -321,12 +335,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Empty);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Empty
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) starts
last_frame_time = 20;
@ -341,12 +369,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) first beat continues
last_frame_time = 30;
@ -361,12 +403,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) begin second beat
last_frame_time = 40;
@ -381,12 +437,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) second beat continues
last_frame_time = 50;
@ -401,12 +471,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) second beat continues
last_frame_time = 60;
@ -421,12 +505,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Record track (0, 0) 3rd beat starts
last_frame_time = 70;
@ -441,12 +539,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Press record end
last_frame_time = 80;
@ -461,12 +573,26 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().all(|f| *f == last_frame_time as f32), "expected to hear the input signal");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.all(|f| *f == last_frame_time as f32),
"expected to hear the input signal"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Recording { volume: 1.0 });
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Recording { volume: 1.0 }
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Track (0, 0) stops recording, starts playing first beat
last_frame_time = 90;
@ -481,13 +607,32 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == last_frame_time as f32), "expected to hear the input signal for the first part");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording started at frame 24");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == last_frame_time as f32),
"expected to hear the input signal for the first part"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 20.0),
"expected to hear the input + recording started at frame 24"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 0);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Press down
last_frame_time = 100;
@ -502,13 +647,32 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording started at frame 24, second buffer");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 30.0),
"expected to hear the input + recording started at frame 24, second buffer"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 40.0),
"expected to hear the input + recording started at frame 24, 3rd buffer"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Press record
last_frame_time = 110;
@ -523,13 +687,32 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording started at frame 24, 3rd buffer");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording started at frame 24, 4th buffer");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 40.0),
"expected to hear the input + recording started at frame 24, 3rd buffer"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 50.0),
"expected to hear the input + recording started at frame 24, 4th buffer"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Empty);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Empty
);
// Recording track (0, 1) starts recording at second column beat
last_frame_time = 120;
@ -544,13 +727,36 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 50.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 60.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Recording track (0, 1) continues recording
last_frame_time = 130;
@ -565,13 +771,36 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 60.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 70.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Track (0, 1) records for second beat, column is at 3rd beat
last_frame_time = 140;
@ -586,13 +815,36 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 70.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 70.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 80.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Column beat 3 continues
last_frame_time = 150;
@ -607,13 +859,36 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 80.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 80.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 90.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Column wraps to first beat, track (0, 1) records 3rd beat
last_frame_time = 160;
@ -628,13 +903,36 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 90.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 90.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 20.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Continue playback and recording
last_frame_time = 170;
@ -649,13 +947,30 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 20.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 30.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Continue playback and recording
last_frame_time = 180;
@ -670,13 +985,30 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 30.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 40.0),
"expected to hear the input + recording of first track"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::RecordingAutoStop { volume: 1.0, target_samples: 72, sync_offset: 24 });
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::RecordingAutoStop {
volume: 1.0,
target_samples: 72,
sync_offset: 24
}
);
// Recording track (0, 1) auto stops, mixed playback starts
last_frame_time = 190;
@ -691,17 +1023,37 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0), "expected to hear the input + recording of first track");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 40.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 120.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(click_output_buffer.iter().any(|f| *f == 1.0), "expected click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 50.0),
"expected to hear the input + recording of first track"
);
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 50.0 + 120.0),
"expected to hear the input + track (0, 0) + track (0, 1)"
);
assert!(
click_output_buffer.iter().any(|f| *f == 1.0),
"expected click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Playing
);
// Process audio
let prh_result = tokio::time::timeout(std::time::Duration::from_secs(1), post_record_handler.run()).await;
let prh_result =
tokio::time::timeout(std::time::Duration::from_secs(1), post_record_handler.run())
.await;
assert!(prh_result.is_err(), "Expected timeout, got {prh_result:#?}");
// Playback consolidated
@ -717,12 +1069,25 @@ mod tests {
midi_output_events: vec![],
};
handler.process(&mut backend).unwrap();
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 50.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(audio_output_buffer.iter().any(|f| *f == (last_frame_time as f32) + 60.0 + 130.0), "expected to hear the input + track (0, 0) + track (0, 1)");
assert!(click_output_buffer.iter().all(|f| *f == 0.0), "expected no click");
assert!(
audio_output_buffer
.iter()
.any(|f| *f == (last_frame_time as f32) + 60.0 + 130.0),
"expected to hear the input + track (0, 0) + track (0, 1)"
);
assert!(
click_output_buffer.iter().all(|f| *f == 0.0),
"expected no click"
);
assert_eq!(handler.selected_column, 0);
assert_eq!(handler.selected_row, 1);
assert_eq!(handler.track_matrix.columns[0].tracks[0].current_state, TrackState::Playing);
assert_eq!(handler.track_matrix.columns[0].tracks[1].current_state, TrackState::Playing);
assert_eq!(
handler.track_matrix.columns[0].tracks[0].current_state,
TrackState::Playing
);
assert_eq!(
handler.track_matrix.columns[0].tracks[1].current_state,
TrackState::Playing
);
}
}
}

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 => {
self.audio_data.copy_samples_to_output(
output_buffer,
playback_position,
self.volume,
)?;
if self.audio_data.len() > 0 {
// Wrap playback position within the track's audio data length
let wrapped_position = playback_position % self.audio_data.len();
// Check if we need to wrap around during this read
if wrapped_position + output_buffer.len() <= self.audio_data.len() {
// Simple case: read fits entirely within the audio data
self.audio_data.copy_samples_to_output(
output_buffer,
wrapped_position,
self.volume,
)?;
} else {
// Complex case: need to wrap around during the read
let samples_until_end = self.audio_data.len() - wrapped_position;
// Read first part (from wrapped_position to end of audio data)
self.audio_data.copy_samples_to_output(
&mut output_buffer[..samples_until_end],
wrapped_position,
self.volume,
)?;
// Read second part (from start of audio data)
self.audio_data.copy_samples_to_output(
&mut output_buffer[samples_until_end..],
0,
self.volume,
)?;
}
} else {
// No audio data to play, output silence
output_buffer.fill(0.0);
}
}
}

View File

@ -90,4 +90,4 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
Ok(())
}
}
}