test: add unit tests for clip state machine, recording, playback, and engine

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-04-30 22:31:31 +00:00
parent 5ccc29ff82
commit b3ab709a7e

View File

@@ -193,3 +193,429 @@ impl Engine {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper to create a test engine
fn test_engine() -> Engine {
Engine::new(2, 44100.0)
}
// Helper to create a test clip with some samples
fn test_clip_with_samples() -> Clip {
let mut clip = Clip::new(2, 44100.0);
clip.samples = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; // 3 stereo frames
clip.state = ClipState::Looping;
clip
}
// ===== ClipState tests =====
#[test]
fn test_clip_initial_state_is_empty() {
let clip = Clip::new(2, 44100.0);
assert_eq!(clip.state, ClipState::Empty);
}
#[test]
fn test_clip_state_velocity_values() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Empty;
assert_eq!(clip.state_velocity(), 0);
clip.state = ClipState::Recording;
assert_eq!(clip.state_velocity(), 64);
clip.state = ClipState::Looping;
assert_eq!(clip.state_velocity(), 127);
clip.state = ClipState::Stopped;
assert_eq!(clip.state_velocity(), 32);
}
// ===== Trigger state machine tests =====
#[test]
fn test_trigger_empty_starts_recording() {
let mut clip = Clip::new(2, 44100.0);
assert_eq!(clip.state, ClipState::Empty);
let new_state = clip.trigger();
assert_eq!(new_state, ClipState::Recording);
assert_eq!(clip.state, ClipState::Recording);
assert!(clip.samples.is_empty());
assert_eq!(clip.write_position, 0);
assert_eq!(clip.read_position, 0);
}
#[test]
fn test_trigger_recording_starts_looping() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Recording;
clip.samples = vec![0.1, 0.2, 0.3, 0.4];
clip.write_position = 4;
let new_state = clip.trigger();
assert_eq!(new_state, ClipState::Looping);
assert_eq!(clip.state, ClipState::Looping);
assert_eq!(clip.read_position, 0);
// Samples should be preserved
assert_eq!(clip.samples.len(), 4);
}
#[test]
fn test_trigger_looping_stops() {
let mut clip = test_clip_with_samples();
assert_eq!(clip.state, ClipState::Looping);
let new_state = clip.trigger();
assert_eq!(new_state, ClipState::Stopped);
assert_eq!(clip.state, ClipState::Stopped);
// Samples should be preserved
assert!(!clip.samples.is_empty());
}
#[test]
fn test_trigger_stopped_starts_looping() {
let mut clip = test_clip_with_samples();
clip.state = ClipState::Stopped;
let new_state = clip.trigger();
assert_eq!(new_state, ClipState::Looping);
assert_eq!(clip.state, ClipState::Looping);
assert_eq!(clip.read_position, 0);
}
#[test]
fn test_trigger_cycle_empty_recording_looping_stopped_looping() {
let mut clip = Clip::new(2, 44100.0);
// Empty -> Recording
assert_eq!(clip.trigger(), ClipState::Recording);
// Recording -> Looping
assert_eq!(clip.trigger(), ClipState::Looping);
// Looping -> Stopped
assert_eq!(clip.trigger(), ClipState::Stopped);
// Stopped -> Looping
assert_eq!(clip.trigger(), ClipState::Looping);
// Looping -> Stopped again
assert_eq!(clip.trigger(), ClipState::Stopped);
}
// ===== Recording tests =====
#[test]
fn test_record_when_not_recording_does_nothing() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Empty;
let input = vec![0.5, 0.6, 0.7, 0.8];
clip.record(&input, 2);
assert!(clip.samples.is_empty());
assert_eq!(clip.write_position, 0);
}
#[test]
fn test_record_appends_samples() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Recording;
let input = vec![0.1, 0.2, 0.3, 0.4];
clip.record(&input, 2);
assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4]);
assert_eq!(clip.write_position, 4);
}
#[test]
fn test_record_multiple_blocks() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Recording;
let input1 = vec![0.1, 0.2, 0.3, 0.4];
clip.record(&input1, 2);
let input2 = vec![0.5, 0.6, 0.7, 0.8];
clip.record(&input2, 2);
assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]);
assert_eq!(clip.write_position, 8);
}
#[test]
fn test_record_clears_on_new_recording() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Recording;
// Record some samples
let input1 = vec![0.1, 0.2, 0.3, 0.4];
clip.record(&input1, 2);
// Trigger to stop recording
clip.trigger(); // Now Looping
// Trigger again to start new recording
clip.trigger(); // Now Stopped
clip.trigger(); // Now Looping
clip.trigger(); // Now Stopped
// Start new recording
clip.state = ClipState::Recording;
clip.samples.clear();
clip.write_position = 0;
let input2 = vec![0.9, 0.8, 0.7, 0.6];
clip.record(&input2, 2);
assert_eq!(clip.samples, vec![0.9, 0.8, 0.7, 0.6]);
assert_eq!(clip.write_position, 4);
}
// ===== Playback tests =====
#[test]
fn test_play_when_not_looping_returns_zero() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Stopped;
clip.samples = vec![0.1, 0.2, 0.3, 0.4];
let mut output = vec![0.0; 4];
let samples_read = clip.play(&mut output, 2);
assert_eq!(samples_read, 0);
assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn test_play_empty_clip_returns_zero() {
let mut clip = Clip::new(2, 44100.0);
clip.state = ClipState::Looping;
let mut output = vec![0.0; 4];
let samples_read = clip.play(&mut output, 2);
assert_eq!(samples_read, 0);
}
#[test]
fn test_play_outputs_samples() {
let mut clip = test_clip_with_samples();
let mut output = vec![0.0; 6]; // 3 stereo frames
let samples_read = clip.play(&mut output, 3);
assert_eq!(samples_read, 6);
assert_eq!(output, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
assert_eq!(clip.read_position, 3);
}
#[test]
fn test_play_loops_when_reaching_end() {
let mut clip = test_clip_with_samples(); // 3 stereo frames
// Read all 3 frames
let mut output1 = vec![0.0; 6];
clip.play(&mut output1, 3);
assert_eq!(clip.read_position, 3); // At end
// Read more - should loop back
let mut output2 = vec![0.0; 4]; // 2 stereo frames
let samples_read = clip.play(&mut output2, 2);
assert_eq!(samples_read, 4);
assert_eq!(output2, vec![0.1, 0.2, 0.3, 0.4]);
assert_eq!(clip.read_position, 2);
}
#[test]
fn test_play_multiple_loops() {
let mut clip = test_clip_with_samples(); // 3 stereo frames
// Read 6 frames (2 full loops)
let mut output = vec![0.0; 12];
let samples_read = clip.play(&mut output, 6);
assert_eq!(samples_read, 12);
// First loop
assert_eq!(output[0..6], vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
// Second loop
assert_eq!(output[6..12], vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
assert_eq!(clip.read_position, 0); // Back to start
}
// ===== Engine tests =====
#[test]
fn test_engine_initial_state() {
let engine = test_engine();
assert!(engine.clips.is_empty());
assert_eq!(engine.num_channels, 2);
assert_eq!(engine.sample_rate, 44100.0);
}
#[test]
fn test_engine_get_clip_creates_new_clip() {
let mut engine = test_engine();
let clip = engine.get_clip(60);
assert_eq!(clip.state, ClipState::Empty);
assert_eq!(clip.num_channels, 2);
assert_eq!(clip.sample_rate, 44100.0);
assert_eq!(engine.clips.len(), 1);
}
#[test]
fn test_engine_get_clip_reuses_existing() {
let mut engine = test_engine();
let clip1 = engine.get_clip(60);
let clip2 = engine.get_clip(60);
// Both should point to the same clip
assert_eq!(clip1 as *const _, clip2 as *const _);
assert_eq!(engine.clips.len(), 1);
}
#[test]
fn test_engine_get_clip_different_notes() {
let mut engine = test_engine();
engine.get_clip(60);
engine.get_clip(61);
engine.get_clip(62);
assert_eq!(engine.clips.len(), 3);
}
#[test]
fn test_engine_process_midi_note_on_triggers_clip() {
let mut engine = test_engine();
// First note on should start recording
let velocity = engine.process_midi_note(60, 100, true);
assert_eq!(velocity, 0); // Empty state velocity
let clip = engine.get_clip(60);
assert_eq!(clip.state, ClipState::Recording);
}
#[test]
fn test_engine_process_midi_note_off_passes_through() {
let mut engine = test_engine();
let velocity = engine.process_midi_note(60, 100, false);
assert_eq!(velocity, 100); // Original velocity passed through
}
#[test]
fn test_engine_process_midi_note_cycle() {
let mut engine = test_engine();
// Note on 1: Empty -> Recording (velocity 0)
let v1 = engine.process_midi_note(60, 100, true);
assert_eq!(v1, 0);
assert_eq!(engine.get_clip(60).state, ClipState::Recording);
// Note on 2: Recording -> Looping (velocity 127)
let v2 = engine.process_midi_note(60, 100, true);
assert_eq!(v2, 127);
assert_eq!(engine.get_clip(60).state, ClipState::Looping);
// Note on 3: Looping -> Stopped (velocity 32)
let v3 = engine.process_midi_note(60, 100, true);
assert_eq!(v3, 32);
assert_eq!(engine.get_clip(60).state, ClipState::Stopped);
// Note on 4: Stopped -> Looping (velocity 127)
let v4 = engine.process_midi_note(60, 100, true);
assert_eq!(v4, 127);
assert_eq!(engine.get_clip(60).state, ClipState::Looping);
}
#[test]
fn test_engine_process_audio_records_and_plays() {
let mut engine = test_engine();
// Start recording on note 60
engine.process_midi_note(60, 100, true);
// Process audio - should record input
let input = vec![0.1, 0.2, 0.3, 0.4];
let mut output = vec![0.0; 4];
engine.process_audio(&input, &mut output, 2);
// Output should be silence (nothing playing yet)
assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]);
// Clip should have recorded the input
let clip = engine.get_clip(60);
assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4]);
// Stop recording and start looping
engine.process_midi_note(60, 100, true);
// Process audio - should play back the recorded clip
let input2 = vec![0.0; 4];
let mut output2 = vec![0.0; 4];
engine.process_audio(&input2, &mut output2, 2);
assert_eq!(output2, vec![0.1, 0.2, 0.3, 0.4]);
}
#[test]
fn test_engine_multiple_clips_independent() {
let mut engine = test_engine();
// Trigger note 60 to record
engine.process_midi_note(60, 100, true);
assert_eq!(engine.get_clip(60).state, ClipState::Recording);
// Trigger note 61 to record
engine.process_midi_note(61, 100, true);
assert_eq!(engine.get_clip(61).state, ClipState::Recording);
// Trigger note 60 again to loop
engine.process_midi_note(60, 100, true);
assert_eq!(engine.get_clip(60).state, ClipState::Looping);
// Note 61 should still be recording
assert_eq!(engine.get_clip(61).state, ClipState::Recording);
}
#[test]
fn test_engine_process_audio_multiple_looping_clips() {
let mut engine = test_engine();
// Create and record clip 60
engine.process_midi_note(60, 100, true);
let input1 = vec![0.1, 0.2, 0.3, 0.4];
engine.process_audio(&input1, &mut vec![0.0; 4], 2);
engine.process_midi_note(60, 100, true); // Now looping
// Create and record clip 61
engine.process_midi_note(61, 100, true);
let input2 = vec![0.5, 0.6, 0.7, 0.8];
engine.process_audio(&input2, &mut vec![0.0; 4], 2);
engine.process_midi_note(61, 100, true); // Now looping
// Both should be looping - output should be sum of both clips
let mut output = vec![0.0; 4];
engine.process_audio(&vec![0.0; 4], &mut output, 2);
// Clip 60 plays [0.1, 0.2, 0.3, 0.4]
// Clip 61 plays [0.5, 0.6, 0.7, 0.8]
// Output is sum: [0.6, 0.8, 1.0, 1.2]
assert!((output[0] - 0.6).abs() < 1e-6);
assert!((output[1] - 0.8).abs() < 1e-6);
assert!((output[2] - 1.0).abs() < 1e-6);
assert!((output[3] - 1.2).abs() < 1e-6);
}
}