feat: add JACK audio looper with clip state machine and tests
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
278
test_engine.c
Normal file
278
test_engine.c
Normal file
@@ -0,0 +1,278 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include "engine.h"
|
||||
|
||||
// Test helper
|
||||
static Engine *create_test_engine(void) {
|
||||
Engine *engine = (Engine *)calloc(1, sizeof(Engine));
|
||||
assert(engine != NULL);
|
||||
|
||||
engine->control_channel = 0;
|
||||
engine->sample_rate = 48000;
|
||||
|
||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||
engine->clips[i].state = CLIP_EMPTY;
|
||||
engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
||||
assert(engine->clips[i].buffer != NULL);
|
||||
engine->clips[i].buffer_size = 0;
|
||||
engine->clips[i].write_position = 0;
|
||||
engine->clips[i].read_position = 0;
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
static void destroy_test_engine(Engine *engine) {
|
||||
if (engine) {
|
||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||
free(engine->clips[i].buffer);
|
||||
}
|
||||
free(engine);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Initial state is empty
|
||||
void test_initial_state(void) {
|
||||
printf("Test 1: Initial state is empty... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
for (int i = 0; i < MAX_CLIPS; i++) {
|
||||
assert(engine->clips[i].state == CLIP_EMPTY);
|
||||
assert(engine->clips[i].buffer_size == 0);
|
||||
assert(engine->clips[i].write_position == 0);
|
||||
assert(engine->clips[i].read_position == 0);
|
||||
}
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 2: Trigger empty clip starts recording
|
||||
void test_trigger_empty_starts_recording(void) {
|
||||
printf("Test 2: Trigger empty clip starts recording... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||
assert(engine->clips[0].write_position == 0);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 3: Trigger recording clip stops and starts looping
|
||||
void test_trigger_recording_starts_looping(void) {
|
||||
printf("Test 3: Trigger recording clip stops and starts looping... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Start recording
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Simulate some recording
|
||||
engine->clips[0].write_position = 100;
|
||||
|
||||
// Trigger again to stop recording and start looping
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
assert(engine->clips[0].buffer_size == 100);
|
||||
assert(engine->clips[0].read_position == 0);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 4: Trigger looping clip stops it
|
||||
void test_trigger_looping_stops(void) {
|
||||
printf("Test 4: Trigger looping clip stops it... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Set up a looping clip
|
||||
engine->clips[0].state = CLIP_LOOPING;
|
||||
engine->clips[0].buffer_size = 100;
|
||||
engine->clips[0].read_position = 50;
|
||||
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_STOPPED);
|
||||
assert(engine->clips[0].read_position == 0);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 5: Trigger stopped clip starts looping again
|
||||
void test_trigger_stopped_resumes_looping(void) {
|
||||
printf("Test 5: Trigger stopped clip starts looping again... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Set up a stopped clip
|
||||
engine->clips[0].state = CLIP_STOPPED;
|
||||
engine->clips[0].buffer_size = 100;
|
||||
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
assert(engine->clips[0].read_position == 0);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 6: Full cycle test
|
||||
void test_full_cycle(void) {
|
||||
printf("Test 6: Full cycle test... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Empty -> Recording
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Recording -> Looping
|
||||
engine->clips[0].write_position = 200;
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
assert(engine->clips[0].buffer_size == 200);
|
||||
|
||||
// Looping -> Stopped
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_STOPPED);
|
||||
|
||||
// Stopped -> Looping
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 7: Multiple clips work independently
|
||||
void test_multiple_clips(void) {
|
||||
printf("Test 7: Multiple clips work independently... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Clip 0: Empty -> Recording
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 1: Empty -> Recording
|
||||
engine_trigger_clip(engine, 1);
|
||||
assert(engine->clips[1].state == CLIP_RECORDING);
|
||||
|
||||
// Clip 0: Recording -> Looping
|
||||
engine->clips[0].write_position = 100;
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
assert(engine->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
||||
|
||||
// Clip 2: Empty -> Recording
|
||||
engine_trigger_clip(engine, 2);
|
||||
assert(engine->clips[2].state == CLIP_RECORDING);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 8: Reset clip
|
||||
void test_reset_clip(void) {
|
||||
printf("Test 8: Reset clip... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Set up a clip with data
|
||||
engine->clips[0].state = CLIP_LOOPING;
|
||||
engine->clips[0].buffer_size = 100;
|
||||
engine->clips[0].write_position = 100;
|
||||
engine->clips[0].read_position = 50;
|
||||
|
||||
engine_reset_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_EMPTY);
|
||||
assert(engine->clips[0].buffer_size == 0);
|
||||
assert(engine->clips[0].write_position == 0);
|
||||
assert(engine->clips[0].read_position == 0);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 9: Clip state to velocity mapping
|
||||
void test_state_to_velocity(void) {
|
||||
printf("Test 9: Clip state to velocity mapping... ");
|
||||
|
||||
assert(clip_state_to_velocity(CLIP_EMPTY) == 0);
|
||||
assert(clip_state_to_velocity(CLIP_RECORDING) == 64);
|
||||
assert(clip_state_to_velocity(CLIP_LOOPING) == 127);
|
||||
assert(clip_state_to_velocity(CLIP_STOPPED) == 32);
|
||||
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 10: Clip state to string
|
||||
void test_state_to_string(void) {
|
||||
printf("Test 10: Clip state to string... ");
|
||||
|
||||
assert(strcmp(clip_state_to_string(CLIP_EMPTY), "Empty") == 0);
|
||||
assert(strcmp(clip_state_to_string(CLIP_RECORDING), "Recording") == 0);
|
||||
assert(strcmp(clip_state_to_string(CLIP_LOOPING), "Looping") == 0);
|
||||
assert(strcmp(clip_state_to_string(CLIP_STOPPED), "Stopped") == 0);
|
||||
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 11: Invalid clip index
|
||||
void test_invalid_clip_index(void) {
|
||||
printf("Test 11: Invalid clip index... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// These should not crash
|
||||
engine_trigger_clip(engine, -1);
|
||||
engine_trigger_clip(engine, MAX_CLIPS);
|
||||
engine_reset_clip(engine, -1);
|
||||
engine_reset_clip(engine, MAX_CLIPS);
|
||||
|
||||
// Verify no state changes
|
||||
assert(engine->clips[0].state == CLIP_EMPTY);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 12: Buffer overflow protection
|
||||
void test_buffer_overflow(void) {
|
||||
printf("Test 12: Buffer overflow protection... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Start recording
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_RECORDING);
|
||||
|
||||
// Fill buffer to max
|
||||
engine->clips[0].write_position = MAX_BUFFER_SIZE;
|
||||
|
||||
// Trigger should stop recording and start looping
|
||||
engine_trigger_clip(engine, 0);
|
||||
assert(engine->clips[0].state == CLIP_LOOPING);
|
||||
assert(engine->clips[0].buffer_size == MAX_BUFFER_SIZE);
|
||||
|
||||
destroy_test_engine(engine);
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Running JACK Looper tests...\n\n");
|
||||
|
||||
test_initial_state();
|
||||
test_trigger_empty_starts_recording();
|
||||
test_trigger_recording_starts_looping();
|
||||
test_trigger_looping_stops();
|
||||
test_trigger_stopped_resumes_looping();
|
||||
test_full_cycle();
|
||||
test_multiple_clips();
|
||||
test_reset_clip();
|
||||
test_state_to_velocity();
|
||||
test_state_to_string();
|
||||
test_invalid_clip_index();
|
||||
test_buffer_overflow();
|
||||
|
||||
printf("\nAll tests passed!\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user