Files
jack-looper/test_engine.c
Loic Coenen 7b23c75dd1 feat: implement lock-free command queue and atomic state for thread safety
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-01 14:52:53 +00:00

657 lines
21 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdatomic.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;
engine->quantize_mode = QUANTIZE_OFF;
engine->quantize_threshold = 0;
engine->queued_triggers = NULL;
// Initialize command queue
command_queue_init(&engine->command_queue);
// Initialize atomic state mirrors
atomic_store(&engine->transport_rolling, 0);
atomic_store(&engine->transport_clock_count, 0);
atomic_store(&engine->transport_beat_position, 0);
atomic_store(&engine->transport_bar_position, 0);
atomic_store(&engine->transport_sample_position, 0);
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
atomic_store(&engine->quantize_threshold_atomic, 0);
// Initialize transport
engine->transport.rolling = false;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
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) {
// Free queued triggers
QueuedTrigger *qt = engine->queued_triggers;
while (qt) {
QueuedTrigger *next = qt->next;
free(qt);
qt = next;
}
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");
}
// Test 13: Transport initial state
void test_transport_initial_state(void) {
printf("Test 13: Transport initial state... ");
Engine *engine = create_test_engine();
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0);
assert(engine->transport.sample_position == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 14: Transport reset
void test_transport_reset(void) {
printf("Test 14: Transport reset... ");
Engine *engine = create_test_engine();
// Simulate some transport state
engine->transport.rolling = true;
engine->transport.clock_count = 100;
engine->transport.beat_position = 2;
engine->transport.bar_position = 5;
engine->transport.sample_position = 10000;
engine_reset_transport(engine);
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0);
assert(engine->transport.sample_position == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 15: Quantize mode setting
void test_quantize_mode_setting(void) {
printf("Test 15: Quantize mode setting... ");
Engine *engine = create_test_engine();
assert(engine->quantize_mode == QUANTIZE_OFF);
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
assert(engine->quantize_mode == QUANTIZE_BEAT);
engine_set_quantize_mode(engine, QUANTIZE_BAR);
assert(engine->quantize_mode == QUANTIZE_BAR);
engine_set_quantize_mode(engine, QUANTIZE_OFF);
assert(engine->quantize_mode == QUANTIZE_OFF);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 16: Quantize mode to string
void test_quantize_mode_to_string(void) {
printf("Test 16: Quantize mode to string... ");
assert(strcmp(quantize_mode_to_string(QUANTIZE_OFF), "Off") == 0);
assert(strcmp(quantize_mode_to_string(QUANTIZE_BEAT), "Beat") == 0);
assert(strcmp(quantize_mode_to_string(QUANTIZE_BAR), "Bar") == 0);
printf("PASSED\n");
}
// Test 17: Quantize threshold setting
void test_quantize_threshold_setting(void) {
printf("Test 17: Quantize threshold setting... ");
Engine *engine = create_test_engine();
assert(engine->quantize_threshold == 0);
engine_set_quantize_threshold(engine, 1000);
assert(engine->quantize_threshold == 1000);
engine_set_quantize_threshold(engine, 0);
assert(engine->quantize_threshold == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 18: Queued trigger management
void test_queued_triggers(void) {
printf("Test 18: Queued trigger management... ");
Engine *engine = create_test_engine();
// Initially no queued triggers
assert(engine->queued_triggers == NULL);
// Queue a clip trigger
queue_trigger(engine, 5, false, 100);
assert(engine->queued_triggers != NULL);
assert(engine->queued_triggers->clip_index == 5);
assert(engine->queued_triggers->is_scene == false);
assert(engine->queued_triggers->trigger_time == 100);
// Queue a scene trigger
queue_trigger(engine, 2, true, 200);
assert(engine->queued_triggers->next != NULL);
assert(engine->queued_triggers->next->clip_index == 2);
assert(engine->queued_triggers->next->is_scene == true);
assert(engine->queued_triggers->next->trigger_time == 200);
// Clean up
QueuedTrigger *qt = engine->queued_triggers;
while (qt) {
QueuedTrigger *next = qt->next;
free(qt);
qt = next;
}
engine->queued_triggers = NULL;
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 19: MIDI clock processing - start message
void test_midi_clock_start(void) {
printf("Test 19: MIDI clock start message... ");
Engine *engine = create_test_engine();
// Simulate receiving MIDI Start (0xFA)
engine->transport.clock_count = 50;
engine->transport.beat_position = 2;
engine->transport.bar_position = 3;
engine->transport.sample_position = 5000;
// Process start message (simplified - just call the logic directly)
engine->transport.rolling = true;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
engine->transport.sample_position = 0;
assert(engine->transport.rolling == true);
assert(engine->transport.clock_count == 0);
assert(engine->transport.beat_position == 0);
assert(engine->transport.bar_position == 0);
assert(engine->transport.sample_position == 0);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 20: MIDI clock processing - stop message
void test_midi_clock_stop(void) {
printf("Test 20: MIDI clock stop message... ");
Engine *engine = create_test_engine();
engine->transport.rolling = true;
engine->transport.clock_count = 100;
// Process stop message
engine->transport.rolling = false;
assert(engine->transport.rolling == false);
assert(engine->transport.clock_count == 100); // Keep position
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 21: MIDI clock processing - continue message
void test_midi_clock_continue(void) {
printf("Test 21: MIDI clock continue message... ");
Engine *engine = create_test_engine();
engine->transport.rolling = false;
engine->transport.clock_count = 100;
// Process continue message
engine->transport.rolling = true;
assert(engine->transport.rolling == true);
assert(engine->transport.clock_count == 100); // Keep position
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 22: Beat tracking from clock ticks
void test_beat_tracking(void) {
printf("Test 22: Beat tracking from clock ticks... ");
Engine *engine = create_test_engine();
engine->transport.rolling = true;
engine->transport.clock_count = 0;
engine->transport.beat_position = 0;
engine->transport.bar_position = 0;
// Simulate 24 clock ticks (one beat)
for (int i = 0; i < MIDI_CLOCKS_PER_BEAT; i++) {
engine->transport.clock_count++;
if (engine->transport.clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
engine->transport.beat_position =
(engine->transport.beat_position + 1) % BEATS_PER_BAR;
if (engine->transport.beat_position == 0) {
engine->transport.bar_position++;
}
}
}
assert(engine->transport.beat_position == 1);
assert(engine->transport.bar_position == 0);
assert(engine->transport.clock_count == MIDI_CLOCKS_PER_BEAT);
// Simulate 3 more beats (total 4 beats = 1 bar)
for (int i = 0; i < MIDI_CLOCKS_PER_BEAT * 3; i++) {
engine->transport.clock_count++;
if (engine->transport.clock_count % MIDI_CLOCKS_PER_BEAT == 0) {
engine->transport.beat_position =
(engine->transport.beat_position + 1) % BEATS_PER_BAR;
if (engine->transport.beat_position == 0) {
engine->transport.bar_position++;
}
}
}
assert(engine->transport.beat_position == 0); // Wrapped around
assert(engine->transport.bar_position == 1); // One full bar
assert(engine->transport.clock_count == MIDI_CLOCKS_PER_BEAT * 4);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 23: Sample position calculation from clock
void test_sample_position_calculation(void) {
printf("Test 23: Sample position calculation from clock... ");
Engine *engine = create_test_engine();
engine->sample_rate = 48000;
// After 24 clocks (1 beat at 120 BPM), sample position should be:
// (24 * 48000 * 4) / (24 * 4) = 48000 samples (1 beat)
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT;
engine->transport.sample_position =
(engine->transport.clock_count * engine->sample_rate * 4) /
(MIDI_CLOCKS_PER_BEAT * BEATS_PER_BAR);
assert(engine->transport.sample_position == engine->sample_rate); // 1 beat = 48000 samples
// After 96 clocks (4 beats = 1 bar)
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 4;
engine->transport.sample_position =
(engine->transport.clock_count * engine->sample_rate * 4) /
(MIDI_CLOCKS_PER_BEAT * BEATS_PER_BAR);
assert(engine->transport.sample_position == engine->sample_rate * 4); // 1 bar = 192000 samples
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 24: Quantization with transport rolling
void test_quantization_with_transport(void) {
printf("Test 24: Quantization with transport rolling... ");
Engine *engine = create_test_engine();
engine->sample_rate = 48000;
engine->transport.rolling = true;
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in
engine->transport.sample_position = engine->sample_rate * 2; // 2 beats in samples
// Set quantize to beat
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
// Calculate next beat boundary
jack_nframes_t frames_per_beat = engine->sample_rate; // 48000 at 120 BPM
jack_nframes_t current_pos = engine->transport.sample_position;
jack_nframes_t next_beat = ((current_pos / frames_per_beat) + 1) * frames_per_beat;
jack_nframes_t quantize_frame = next_beat - engine->transport.sample_position;
// Should be 48000 samples to next beat
assert(quantize_frame == frames_per_beat);
// Test bar quantization
engine_set_quantize_mode(engine, QUANTIZE_BAR);
jack_nframes_t frames_per_bar = frames_per_beat * BEATS_PER_BAR;
jack_nframes_t next_bar = ((current_pos / frames_per_bar) + 1) * frames_per_bar;
quantize_frame = next_bar - engine->transport.sample_position;
// Should be 96000 samples to next bar (2 beats into 4-beat bar)
assert(quantize_frame == frames_per_beat * 2);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 25: Quantization off with transport rolling
void test_quantization_off_with_transport(void) {
printf("Test 25: Quantization off with transport rolling... ");
Engine *engine = create_test_engine();
engine->sample_rate = 48000;
engine->transport.rolling = true;
engine->transport.clock_count = MIDI_CLOCKS_PER_BEAT * 2;
engine->transport.sample_position = engine->sample_rate * 2;
engine_set_quantize_mode(engine, QUANTIZE_OFF);
// When quantize is off, trigger should be immediate
jack_nframes_t current_frame = 100;
jack_nframes_t quantize_frame = current_frame; // Should be same as current
assert(quantize_frame == 100);
destroy_test_engine(engine);
printf("PASSED\n");
}
// Test 26: Quantization without transport rolling
void test_quantization_without_transport(void) {
printf("Test 26: Quantization without transport rolling... ");
Engine *engine = create_test_engine();
engine->sample_rate = 48000;
engine->transport.rolling = false;
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
// When transport is not rolling, trigger should be immediate
jack_nframes_t current_frame = 100;
jack_nframes_t quantize_frame = current_frame; // Should be same as current
assert(quantize_frame == 100);
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();
test_transport_initial_state();
test_transport_reset();
test_quantize_mode_setting();
test_quantize_mode_to_string();
test_quantize_threshold_setting();
test_queued_triggers();
test_midi_clock_start();
test_midi_clock_stop();
test_midi_clock_continue();
test_beat_tracking();
test_sample_position_calculation();
test_quantization_with_transport();
test_quantization_off_with_transport();
test_quantization_without_transport();
printf("\nAll tests passed!\n");
return 0;
}