From 0d02812865e93488a270499549951d9164ea18f1 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sat, 2 May 2026 15:54:06 +0000 Subject: [PATCH] fix: update test_engine.c to use AppState/reducer and fix duplicate enum in transport.h Co-authored-by: aider (deepseek/deepseek-coder) --- test_engine.c | 773 +++++++++++++++++++++++++++----------------------- transport.h | 11 +- 2 files changed, 412 insertions(+), 372 deletions(-) diff --git a/test_engine.c b/test_engine.c index 51517f9..2d80813 100644 --- a/test_engine.c +++ b/test_engine.c @@ -7,238 +7,237 @@ #include "transport.h" #include "engine.h" -// Test helper -static Engine *create_test_engine(void) { - Engine *engine = (Engine *)calloc(1, sizeof(Engine)); - assert(engine != NULL); +// ============================================================ +// Test helpers +// ============================================================ + +static AppState create_test_state(void) { + AppState state; + memset(&state, 0, sizeof(state)); - 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); + state.transport_state = TRANSPORT_STOPPED; + state.clock_source = CLOCK_SOURCE_INTERNAL; + state.bpm = DEFAULT_BPM; + state.sample_rate = 48000; + state.samples_per_beat = (state.sample_rate * 60.0) / state.bpm; + state.sample_accumulator = 0.0; + state.quantize_mode = QUANTIZE_OFF; + state.quantize_threshold = 0; + state.running = true; 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; + state.clips[i].state = CLIP_EMPTY; + state.clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + assert(state.clips[i].buffer != NULL); + state.clips[i].buffer_size = 0; + state.clips[i].write_position = 0; + state.clips[i].read_position = 0; } - return engine; + return state; } -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); - } +static void destroy_test_state(AppState *state) { + if (state) { for (int i = 0; i < MAX_CLIPS; i++) { - free(engine->clips[i].buffer); + free(state->clips[i].buffer); + state->clips[i].buffer = NULL; } - 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(); + AppState state = create_test_state(); 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); + assert(state.clips[i].state == CLIP_EMPTY); + assert(state.clips[i].buffer_size == 0); + assert(state.clips[i].write_position == 0); + assert(state.clips[i].read_position == 0); } - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_RECORDING); - assert(engine->clips[0].write_position == 0); + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + state = reducer(state, action); - destroy_test_engine(engine); + assert(state.clips[0].state == CLIP_RECORDING); + assert(state.clips[0].write_position == 0); + + destroy_test_state(&state); 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(); + AppState state = create_test_state(); // Start recording - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_RECORDING); + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + state = reducer(state, action); + assert(state.clips[0].state == CLIP_RECORDING); // Simulate some recording - engine->clips[0].write_position = 100; + state.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); + state = reducer(state, action); + assert(state.clips[0].state == CLIP_LOOPING); + assert(state.clips[0].buffer_size == 100); + assert(state.clips[0].read_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); // Set up a looping clip - engine->clips[0].state = CLIP_LOOPING; - engine->clips[0].buffer_size = 100; - engine->clips[0].read_position = 50; + state.clips[0].state = CLIP_LOOPING; + state.clips[0].buffer_size = 100; + state.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); + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + state = reducer(state, action); + assert(state.clips[0].state == CLIP_STOPPED); + assert(state.clips[0].read_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); // Set up a stopped clip - engine->clips[0].state = CLIP_STOPPED; - engine->clips[0].buffer_size = 100; + state.clips[0].state = CLIP_STOPPED; + state.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); + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + state = reducer(state, action); + assert(state.clips[0].state == CLIP_LOOPING); + assert(state.clips[0].read_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 6: Full cycle test +// ============================================================ void test_full_cycle(void) { printf("Test 6: Full cycle test... "); - Engine *engine = create_test_engine(); + AppState state = create_test_state(); + + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; // Empty -> Recording - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_RECORDING); + state = reducer(state, action); + assert(state.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); + state.clips[0].write_position = 200; + state = reducer(state, action); + assert(state.clips[0].state == CLIP_LOOPING); + assert(state.clips[0].buffer_size == 200); // Looping -> Stopped - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_STOPPED); + state = reducer(state, action); + assert(state.clips[0].state == CLIP_STOPPED); // Stopped -> Looping - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_LOOPING); + state = reducer(state, action); + assert(state.clips[0].state == CLIP_LOOPING); - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); + + Action action0 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + Action action1 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 1 } }; + Action action2 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 2 } }; // Clip 0: Empty -> Recording - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_RECORDING); + state = reducer(state, action0); + assert(state.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); + state = reducer(state, action1); + assert(state.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 + state.clips[0].write_position = 100; + state = reducer(state, action0); + assert(state.clips[0].state == CLIP_LOOPING); + assert(state.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); + state = reducer(state, action2); + assert(state.clips[2].state == CLIP_RECORDING); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 8: Reset clip +// ============================================================ void test_reset_clip(void) { printf("Test 8: Reset clip... "); - Engine *engine = create_test_engine(); + AppState state = create_test_state(); // 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; + state.clips[0].state = CLIP_LOOPING; + state.clips[0].buffer_size = 100; + state.clips[0].write_position = 100; + state.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); + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } }; + state = reducer(state, action); + assert(state.clips[0].state == CLIP_EMPTY); + assert(state.clips[0].buffer_size == 0); + assert(state.clips[0].write_position == 0); + assert(state.clips[0].read_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 9: Clip state to velocity mapping +// ============================================================ void test_state_to_velocity(void) { printf("Test 9: Clip state to velocity mapping... "); @@ -250,7 +249,9 @@ void test_state_to_velocity(void) { printf("PASSED\n"); } +// ============================================================ // Test 10: Clip state to string +// ============================================================ void test_state_to_string(void) { printf("Test 10: Clip state to string... "); @@ -262,111 +263,129 @@ void test_state_to_string(void) { 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(); + AppState state = create_test_state(); // 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); + Action action_neg = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = -1 } }; + Action action_max = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = MAX_CLIPS } }; + Action reset_neg = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = -1 } }; + Action reset_max = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = MAX_CLIPS } }; + + state = reducer(state, action_neg); + state = reducer(state, action_max); + state = reducer(state, reset_neg); + state = reducer(state, reset_max); // Verify no state changes - assert(engine->clips[0].state == CLIP_EMPTY); + assert(state.clips[0].state == CLIP_EMPTY); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 12: Buffer overflow protection +// ============================================================ void test_buffer_overflow(void) { printf("Test 12: Buffer overflow protection... "); - Engine *engine = create_test_engine(); + AppState state = create_test_state(); + + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; // Start recording - engine_trigger_clip(engine, 0); - engine_process_commands(engine); - assert(engine->clips[0].state == CLIP_RECORDING); + state = reducer(state, action); + assert(state.clips[0].state == CLIP_RECORDING); // Fill buffer to max - engine->clips[0].write_position = MAX_BUFFER_SIZE; + state.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); + state = reducer(state, action); + assert(state.clips[0].state == CLIP_LOOPING); + assert(state.clips[0].buffer_size == MAX_BUFFER_SIZE); - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); - 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); + assert(state.transport_state == TRANSPORT_STOPPED); + assert(state.clock_count == 0); + assert(state.beat_position == 0); + assert(state.bar_position == 0); + assert(state.sample_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 14: Transport reset +// ============================================================ void test_transport_reset(void) { printf("Test 14: Transport reset... "); - Engine *engine = create_test_engine(); + AppState state = create_test_state(); // 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; + state.transport_state = TRANSPORT_PLAYING; + state.clock_count = 100; + state.beat_position = 2; + state.bar_position = 5; + state.sample_position = 10000; - engine_transport_stop(engine); - engine_process_commands(engine); + Action action = { .type = ACTION_RESET_TRANSPORT }; + state = reducer(state, action); - 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); + assert(state.transport_state == TRANSPORT_STOPPED); + assert(state.clock_count == 0); + assert(state.beat_position == 0); + assert(state.bar_position == 0); + assert(state.sample_position == 0); - destroy_test_engine(engine); + destroy_test_state(&state); 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(); + AppState state = create_test_state(); - assert(engine->quantize_mode == QUANTIZE_OFF); + assert(state.quantize_mode == QUANTIZE_OFF); - engine_set_quantize_mode(engine, QUANTIZE_BEAT); - engine_process_commands(engine); - assert(engine->quantize_mode == QUANTIZE_BEAT); + Action action_beat = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; + Action action_bar = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BAR } }; + Action action_off = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } }; - engine_set_quantize_mode(engine, QUANTIZE_BAR); - engine_process_commands(engine); - assert(engine->quantize_mode == QUANTIZE_BAR); + state = reducer(state, action_beat); + assert(state.quantize_mode == QUANTIZE_BEAT); - engine_set_quantize_mode(engine, QUANTIZE_OFF); - engine_process_commands(engine); - assert(engine->quantize_mode == QUANTIZE_OFF); + state = reducer(state, action_bar); + assert(state.quantize_mode == QUANTIZE_BAR); - destroy_test_engine(engine); + state = reducer(state, action_off); + assert(state.quantize_mode == QUANTIZE_OFF); + + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ // Test 16: Quantize mode to string +// ============================================================ void test_quantize_mode_to_string(void) { printf("Test 16: Quantize mode to string... "); @@ -377,241 +396,212 @@ void test_quantize_mode_to_string(void) { 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(); + AppState state = create_test_state(); - assert(engine->quantize_threshold == 0); + assert(state.quantize_threshold == 0); - engine_set_quantize_threshold(engine, 1000); - engine_process_commands(engine); - assert(engine->quantize_threshold == 1000); + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold = { .threshold = 1000 } }; + state = reducer(state, action); + assert(state.quantize_threshold == 1000); - engine_set_quantize_threshold(engine, 0); - engine_process_commands(engine); - assert(engine->quantize_threshold == 0); + action.data.set_quantize_threshold.threshold = 0; + state = reducer(state, action); + assert(state.quantize_threshold == 0); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } -// Test 18: Queued trigger management -void test_queued_triggers(void) { - printf("Test 18: Queued trigger management... "); - Engine *engine = create_test_engine(); +// ============================================================ +// Test 18: Undo/Redo basic +// ============================================================ +void test_undo_redo(void) { + printf("Test 18: Undo/Redo basic... "); + AppState state = create_test_state(); - // Initially no queued triggers - assert(engine->queued_triggers == NULL); + // Trigger clip 0 (empty -> recording) + Action trigger = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; + state = reducer(state, trigger); + assert(state.clips[0].state == CLIP_RECORDING); - // 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); + // Undo + Action undo = { .type = ACTION_UNDO }; + state = reducer(state, undo); + assert(state.clips[0].state == CLIP_EMPTY); - // 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); + // Redo + Action redo = { .type = ACTION_REDO }; + state = reducer(state, redo); + assert(state.clips[0].state == CLIP_RECORDING); - // 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); + destroy_test_state(&state); 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(); +// ============================================================ +// Test 19: MIDI note on triggers clip +// ============================================================ +void test_midi_note_on(void) { + printf("Test 19: MIDI note on triggers clip... "); + AppState state = create_test_state(); - // 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; + Action action = { .type = ACTION_MIDI_NOTE_ON, .data.midi_note_on = { .note = 60, .velocity = 100, .channel = 0, .time = 0 } }; + state = reducer(state, action); - // 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; + int clip_idx = 60 % MAX_CLIPS; + assert(state.clips[clip_idx].state == CLIP_RECORDING); - assert(engine->transport->state == TRANSPORT_PLAYING); - 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); + destroy_test_state(&state); 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(); +// ============================================================ +// Test 20: Scene trigger +// ============================================================ +void test_scene_trigger(void) { + printf("Test 20: Scene trigger... "); + AppState state = create_test_state(); - engine->transport->state = TRANSPORT_PLAYING; - engine->transport->clock_count = 100; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = 0 } }; + state = reducer(state, action); - // Process stop message - engine->transport->state = TRANSPORT_STOPPED; - - assert(engine->transport->state == TRANSPORT_STOPPED); - assert(engine->transport->clock_count == 100); - - 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); - - 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->state = TRANSPORT_PLAYING; - 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++; - } - } + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(0, ch); + assert(state.clips[clip_idx].state == CLIP_RECORDING); } - 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); - assert(engine->transport->bar_position == 1); - assert(engine->transport->clock_count == MIDI_CLOCKS_PER_BEAT * 4); - - destroy_test_engine(engine); + destroy_test_state(&state); 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; +// ============================================================ +// Test 21: Transport play/pause/stop +// ============================================================ +void test_transport_play_pause_stop(void) { + printf("Test 21: Transport play/pause/stop... "); + AppState state = create_test_state(); - // 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); + Action play = { .type = ACTION_TRANSPORT_PLAY }; + Action pause = { .type = ACTION_TRANSPORT_PAUSE }; + Action stop = { .type = ACTION_TRANSPORT_STOP }; + Action toggle = { .type = ACTION_TRANSPORT_TOGGLE_PLAY }; - assert(engine->transport->sample_position == engine->sample_rate); // 1 beat = 48000 samples + state = reducer(state, play); + assert(state.transport_state == TRANSPORT_PLAYING); - // 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); + state = reducer(state, pause); + assert(state.transport_state == TRANSPORT_PAUSED); - assert(engine->transport->sample_position == engine->sample_rate * 4); // 1 bar = 192000 samples + state = reducer(state, toggle); + assert(state.transport_state == TRANSPORT_PLAYING); - destroy_test_engine(engine); + state = reducer(state, stop); + assert(state.transport_state == TRANSPORT_STOPPED); + assert(state.clock_count == 0); + assert(state.beat_position == 0); + assert(state.bar_position == 0); + assert(state.sample_position == 0); + + destroy_test_state(&state); printf("PASSED\n"); } -// Test 24: Quantization with transport rolling +// ============================================================ +// Test 22: BPM setting +// ============================================================ +void test_bpm_setting(void) { + printf("Test 22: BPM setting... "); + AppState state = create_test_state(); + + assert(state.bpm == DEFAULT_BPM); + + Action action = { .type = ACTION_SET_BPM, .data.set_bpm = { .bpm = 140.0 } }; + state = reducer(state, action); + assert(state.bpm == 140.0); + double expected_spp = (state.sample_rate * 60.0) / 140.0; + assert(state.samples_per_beat == expected_spp); + + destroy_test_state(&state); + printf("PASSED\n"); +} + +// ============================================================ +// Test 23: Clock source setting +// ============================================================ +void test_clock_source_setting(void) { + printf("Test 23: Clock source setting... "); + AppState state = create_test_state(); + + assert(state.clock_source == CLOCK_SOURCE_INTERNAL); + + Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = CLOCK_SOURCE_MIDI } }; + state = reducer(state, action); + assert(state.clock_source == CLOCK_SOURCE_MIDI); + assert(state.clock_count == 0); + assert(state.beat_position == 0); + assert(state.bar_position == 0); + assert(state.sample_position == 0); + + destroy_test_state(&state); + printf("PASSED\n"); +} + +// ============================================================ +// Test 24: Quantization with transport rolling (simulated) +// ============================================================ 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 + AppState state = create_test_state(); + state.sample_rate = 48000; + state.transport_state = TRANSPORT_PLAYING; + state.clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in + state.sample_position = state.sample_rate * 2; // 2 beats in samples // Set quantize to beat - engine_set_quantize_mode(engine, QUANTIZE_BEAT); + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; + state = reducer(state, action); // 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 frames_per_beat = state.sample_rate; // 48000 at 120 BPM + jack_nframes_t current_pos = state.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; + jack_nframes_t quantize_frame = next_beat - state.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); + action.data.set_quantize_mode.mode = QUANTIZE_BAR; + state = reducer(state, action); 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; + quantize_frame = next_bar - state.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); + destroy_test_state(&state); 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; + AppState state = create_test_state(); + state.sample_rate = 48000; + state.transport_state = TRANSPORT_PLAYING; + state.clock_count = MIDI_CLOCKS_PER_BEAT * 2; + state.sample_position = state.sample_rate * 2; - engine_set_quantize_mode(engine, QUANTIZE_OFF); + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } }; + state = reducer(state, action); // When quantize is off, trigger should be immediate jack_nframes_t current_frame = 100; @@ -619,18 +609,21 @@ void test_quantization_off_with_transport(void) { assert(quantize_frame == 100); - destroy_test_engine(engine); + destroy_test_state(&state); 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; + AppState state = create_test_state(); + state.sample_rate = 48000; + state.transport_state = TRANSPORT_STOPPED; - engine_set_quantize_mode(engine, QUANTIZE_BEAT); + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; + state = reducer(state, action); // When transport is not rolling, trigger should be immediate jack_nframes_t current_frame = 100; @@ -638,10 +631,64 @@ void test_quantization_without_transport(void) { assert(quantize_frame == 100); - destroy_test_engine(engine); + destroy_test_state(&state); printf("PASSED\n"); } +// ============================================================ +// Test 27: Save/Load clip (basic) +// ============================================================ +void test_save_load_clip(void) { + printf("Test 27: Save/Load clip (basic)... "); + AppState state = create_test_state(); + + // Set up a clip with data + state.clips[0].state = CLIP_LOOPING; + state.clips[0].buffer_size = 100; + state.clips[0].write_position = 100; + state.clips[0].read_position = 0; + for (size_t i = 0; i < 100; i++) { + state.clips[0].buffer[i] = (float)i; + } + + // Save clip + Action save = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = 0 } }; + state = reducer(state, save); + + // Reset clip + Action reset = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } }; + state = reducer(state, reset); + assert(state.clips[0].state == CLIP_EMPTY); + + // Load clip (requires a filename; we'll use a dummy that will fail gracefully) + Action load = { .type = ACTION_LOAD_CLIP, .data.load_clip = { .clip_index = 0, .filename = "samples/clip_0.wav" } }; + state = reducer(state, load); + // If file doesn't exist, clip remains empty (no crash) + // We just verify no crash + + destroy_test_state(&state); + printf("PASSED\n"); +} + +// ============================================================ +// Test 28: Quit action +// ============================================================ +void test_quit_action(void) { + printf("Test 28: Quit action... "); + AppState state = create_test_state(); + assert(state.running == true); + + Action quit = { .type = ACTION_QUIT }; + state = reducer(state, quit); + assert(state.running == false); + + destroy_test_state(&state); + printf("PASSED\n"); +} + +// ============================================================ +// Main +// ============================================================ int main(void) { printf("Running JACK Looper tests...\n\n"); @@ -662,15 +709,17 @@ int main(void) { 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_undo_redo(); + test_midi_note_on(); + test_scene_trigger(); + test_transport_play_pause_stop(); + test_bpm_setting(); + test_clock_source_setting(); test_quantization_with_transport(); test_quantization_off_with_transport(); test_quantization_without_transport(); + test_save_load_clip(); + test_quit_action(); printf("\nAll tests passed!\n"); return 0; diff --git a/transport.h b/transport.h index bc47b36..0d39cec 100644 --- a/transport.h +++ b/transport.h @@ -11,16 +11,7 @@ #define BEATS_PER_BAR 4 #define DEFAULT_BPM 120.0 -typedef enum { - TRANSPORT_STOPPED, - TRANSPORT_PLAYING, - TRANSPORT_PAUSED -} TransportState; - -typedef enum { - CLOCK_SOURCE_INTERNAL, // Master clock - CLOCK_SOURCE_MIDI // Slave to external MIDI clock -} ClockSource; +#include "dispatcher.h" typedef struct { // State