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:
426
src/engine.rs
426
src/engine.rs
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user