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