#include #include #include #include #include #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->quantize_mode_atomic, (int)QUANTIZE_OFF); atomic_store(&engine->quantize_threshold_atomic, 0); // Initialize transport engine->transport = (Transport *)calloc(1, sizeof(Transport)); assert(engine->transport != NULL); transport_init(engine->transport, 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) { // Free queued triggers QueuedTrigger *qt = engine->queued_triggers; while (qt) { QueuedTrigger *next = qt->next; free(qt); qt = next; } if (engine->transport) { transport_cleanup(engine->transport); free(engine->transport); } 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); assert(engine->clips[0].state == CLIP_RECORDING); // Recording -> Looping engine->clips[0].write_position = 200; engine_trigger_clip(engine, 0); engine_process_commands(engine); assert(engine->clips[0].state == CLIP_LOOPING); assert(engine->clips[0].buffer_size == 200); // Looping -> Stopped engine_trigger_clip(engine, 0); engine_process_commands(engine); assert(engine->clips[0].state == CLIP_STOPPED); // Stopped -> Looping engine_trigger_clip(engine, 0); engine_process_commands(engine); 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); engine_process_commands(engine); assert(engine->clips[0].state == CLIP_RECORDING); // Clip 1: Empty -> Recording engine_trigger_clip(engine, 1); engine_process_commands(engine); assert(engine->clips[1].state == CLIP_RECORDING); // Clip 0: Recording -> Looping engine->clips[0].write_position = 100; engine_trigger_clip(engine, 0); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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); engine_process_commands(engine); 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->state == TRANSPORT_STOPPED); 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->state = TRANSPORT_PLAYING; engine->transport->clock_count = 100; engine->transport->beat_position = 2; engine->transport->bar_position = 5; engine->transport->sample_position = 10000; engine_reset_transport(engine); engine_process_commands(engine); assert(engine->transport->state == TRANSPORT_STOPPED); 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); engine_process_commands(engine); assert(engine->quantize_mode == QUANTIZE_BEAT); engine_set_quantize_mode(engine, QUANTIZE_BAR); engine_process_commands(engine); assert(engine->quantize_mode == QUANTIZE_BAR); engine_set_quantize_mode(engine, QUANTIZE_OFF); engine_process_commands(engine); 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); engine_process_commands(engine); assert(engine->quantize_threshold == 1000); engine_set_quantize_threshold(engine, 0); engine_process_commands(engine); 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->state = TRANSPORT_PLAYING; 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->state = TRANSPORT_PLAYING; engine->transport->clock_count = 100; // Process stop message engine->transport->state = TRANSPORT_STOPPED; assert(engine->transport->state == TRANSPORT_STOPPED); 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->state = TRANSPORT_STOPPED; engine->transport->clock_count = 100; // Process continue message engine->transport->state = TRANSPORT_PLAYING; assert(engine->transport->state == TRANSPORT_PLAYING); 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->state = TRANSPORT_PLAYING; 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->state = TRANSPORT_PLAYING; 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->state = TRANSPORT_STOPPED; 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; }