1072 lines
38 KiB
C
1072 lines
38 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <stdatomic.h>
|
|
#include "dispatcher.h"
|
|
#include "transport.h"
|
|
#include "engine.h"
|
|
|
|
// ============================================================
|
|
// Test helpers
|
|
// ============================================================
|
|
|
|
static AppState* create_test_state(void) {
|
|
AppState *state = (AppState *)calloc(1, sizeof(AppState));
|
|
assert(state != NULL);
|
|
|
|
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++) {
|
|
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;
|
|
|
|
// Initialize midi clips
|
|
state->midi_clips[i].state = CLIP_EMPTY;
|
|
state->midi_clips[i].events = (MidiEvent *)calloc(MAX_MIDI_EVENTS, sizeof(MidiEvent));
|
|
assert(state->midi_clips[i].events != NULL);
|
|
state->midi_clips[i].max_events = MAX_MIDI_EVENTS;
|
|
state->midi_clips[i].event_count = 0;
|
|
state->midi_clips[i].read_index = 0;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
static void destroy_test_state(AppState *state) {
|
|
if (state) {
|
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
|
free(state->clips[i].buffer);
|
|
state->clips[i].buffer = NULL;
|
|
free(state->midi_clips[i].events);
|
|
state->midi_clips[i].events = NULL;
|
|
}
|
|
free(state);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 1: Initial state is empty
|
|
// ============================================================
|
|
void test_initial_state(void) {
|
|
printf("Test 1: Initial state is empty... ");
|
|
AppState *state = create_test_state();
|
|
|
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
|
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_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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
|
|
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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Start recording
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
// Simulate some recording
|
|
state->clips[0].write_position = 100;
|
|
|
|
// Write data into the ring buffer so the reducer can copy it
|
|
for (size_t i = 0; i < 100; i++) {
|
|
state->record_buffer[0][i] = (float)i;
|
|
}
|
|
atomic_store(&state->record_write_pos[0], 100);
|
|
|
|
// Trigger again to stop recording and start looping
|
|
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_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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a looping clip
|
|
state->clips[0].state = CLIP_LOOPING;
|
|
state->clips[0].buffer_size = 100;
|
|
state->clips[0].read_position = 50;
|
|
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_STOPPED);
|
|
assert(state->clips[0].read_position == 0);
|
|
|
|
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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a stopped clip
|
|
state->clips[0].state = CLIP_STOPPED;
|
|
state->clips[0].buffer_size = 100;
|
|
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_LOOPING);
|
|
assert(state->clips[0].read_position == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 6: Full cycle test
|
|
// ============================================================
|
|
void test_full_cycle(void) {
|
|
printf("Test 6: Full cycle test... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
|
|
// Empty -> Recording
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
// Recording -> Looping
|
|
state->clips[0].write_position = 200;
|
|
|
|
// Write data into the ring buffer so the reducer can copy it
|
|
for (size_t i = 0; i < 200; i++) {
|
|
state->record_buffer[0][i] = (float)i;
|
|
}
|
|
atomic_store(&state->record_write_pos[0], 200);
|
|
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_LOOPING);
|
|
assert(state->clips[0].buffer_size == 200);
|
|
|
|
// Looping -> Stopped
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_STOPPED);
|
|
|
|
// Stopped -> Looping
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_LOOPING);
|
|
|
|
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... ");
|
|
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
|
|
reducer(state, action0);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
// Clip 1: Empty -> Recording
|
|
reducer(state, action1);
|
|
assert(state->clips[1].state == CLIP_RECORDING);
|
|
|
|
// Clip 0: Recording -> Looping
|
|
state->clips[0].write_position = 100;
|
|
reducer(state, action0);
|
|
assert(state->clips[0].state == CLIP_LOOPING);
|
|
assert(state->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
|
|
|
// Clip 2: Empty -> Recording
|
|
reducer(state, action2);
|
|
assert(state->clips[2].state == CLIP_RECORDING);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 8: Reset clip
|
|
// ============================================================
|
|
void test_reset_clip(void) {
|
|
printf("Test 8: Reset clip... ");
|
|
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 = 50;
|
|
|
|
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
|
|
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_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... ");
|
|
|
|
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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// These should not crash
|
|
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 } };
|
|
|
|
reducer(state, action_neg);
|
|
reducer(state, action_max);
|
|
reducer(state, reset_neg);
|
|
reducer(state, reset_max);
|
|
|
|
// Verify no state changes
|
|
assert(state->clips[0].state == CLIP_EMPTY);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 12: Buffer overflow protection
|
|
// ============================================================
|
|
void test_buffer_overflow(void) {
|
|
printf("Test 12: Buffer overflow protection... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
|
|
// Start recording
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
// Fill buffer to max
|
|
state->clips[0].write_position = MAX_BUFFER_SIZE;
|
|
|
|
// Write data into the ring buffer so the reducer can copy it
|
|
for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) {
|
|
state->record_buffer[0][i] = (float)i;
|
|
}
|
|
atomic_store(&state->record_write_pos[0], MAX_BUFFER_SIZE);
|
|
|
|
// Trigger should stop recording and start looping
|
|
reducer(state, action);
|
|
assert(state->clips[0].state == CLIP_LOOPING);
|
|
assert(state->clips[0].buffer_size == MAX_BUFFER_SIZE);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 13: Transport initial state
|
|
// ============================================================
|
|
void test_transport_initial_state(void) {
|
|
printf("Test 13: Transport initial state... ");
|
|
AppState *state = create_test_state();
|
|
|
|
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 14: Transport reset
|
|
// ============================================================
|
|
void test_transport_reset(void) {
|
|
printf("Test 14: Transport reset... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Simulate some transport state
|
|
state->transport_state = TRANSPORT_PLAYING;
|
|
state->clock_count = 100;
|
|
state->beat_position = 2;
|
|
state->bar_position = 5;
|
|
state->sample_position = 10000;
|
|
|
|
Action action = { .type = ACTION_RESET_TRANSPORT };
|
|
reducer(state, action);
|
|
|
|
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 15: Quantize mode setting
|
|
// ============================================================
|
|
void test_quantize_mode_setting(void) {
|
|
printf("Test 15: Quantize mode setting... ");
|
|
AppState *state = create_test_state();
|
|
|
|
assert(state->quantize_mode == QUANTIZE_OFF);
|
|
|
|
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 } };
|
|
|
|
reducer(state, action_beat);
|
|
assert(state->quantize_mode == QUANTIZE_BEAT);
|
|
|
|
reducer(state, action_bar);
|
|
assert(state->quantize_mode == QUANTIZE_BAR);
|
|
|
|
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... ");
|
|
|
|
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... ");
|
|
AppState *state = create_test_state();
|
|
|
|
assert(state->quantize_threshold == 0);
|
|
|
|
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold = { .threshold = 1000 } };
|
|
reducer(state, action);
|
|
assert(state->quantize_threshold == 1000);
|
|
|
|
action.data.set_quantize_threshold.threshold = 0;
|
|
reducer(state, action);
|
|
assert(state->quantize_threshold == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 18: Undo/Redo basic
|
|
// ============================================================
|
|
void test_undo_redo(void) {
|
|
printf("Test 18: Undo/Redo basic... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Trigger clip 0 (empty -> recording)
|
|
Action trigger = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
|
|
reducer(state, trigger);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
// Undo
|
|
Action undo = { .type = ACTION_UNDO };
|
|
reducer(state, undo);
|
|
assert(state->clips[0].state == CLIP_EMPTY);
|
|
|
|
// Redo
|
|
Action redo = { .type = ACTION_REDO };
|
|
reducer(state, redo);
|
|
assert(state->clips[0].state == CLIP_RECORDING);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// 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();
|
|
|
|
Action action = { .type = ACTION_MIDI_NOTE_ON, .data.midi_note_on = { .note = 60, .velocity = 100, .channel = 0, .time = 0 } };
|
|
reducer(state, action);
|
|
|
|
int clip_idx = 60 % MAX_CLIPS;
|
|
assert(state->clips[clip_idx].state == CLIP_RECORDING);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 20: Scene trigger
|
|
// ============================================================
|
|
void test_scene_trigger(void) {
|
|
printf("Test 20: Scene trigger... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = 0 } };
|
|
reducer(state, action);
|
|
|
|
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
|
int clip_idx = CLIP_INDEX(0, ch);
|
|
assert(state->clips[clip_idx].state == CLIP_RECORDING);
|
|
}
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// 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();
|
|
|
|
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 };
|
|
|
|
reducer(state, play);
|
|
assert(state->transport_state == TRANSPORT_PLAYING);
|
|
|
|
reducer(state, pause);
|
|
assert(state->transport_state == TRANSPORT_PAUSED);
|
|
|
|
reducer(state, toggle);
|
|
assert(state->transport_state == TRANSPORT_PLAYING);
|
|
|
|
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 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 } };
|
|
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 } };
|
|
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... ");
|
|
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
|
|
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } };
|
|
reducer(state, action);
|
|
|
|
// Calculate next beat boundary
|
|
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 - state->sample_position;
|
|
|
|
// Should be 48000 samples to next beat
|
|
assert(quantize_frame == frames_per_beat);
|
|
|
|
// Test bar quantization
|
|
action.data.set_quantize_mode.mode = QUANTIZE_BAR;
|
|
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 - 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_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... ");
|
|
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;
|
|
|
|
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } };
|
|
reducer(state, action);
|
|
|
|
// 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_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 26: Quantization without transport rolling
|
|
// ============================================================
|
|
void test_quantization_without_transport(void) {
|
|
printf("Test 26: Quantization without transport rolling... ");
|
|
AppState *state = create_test_state();
|
|
state->sample_rate = 48000;
|
|
state->transport_state = TRANSPORT_STOPPED;
|
|
|
|
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } };
|
|
reducer(state, action);
|
|
|
|
// 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_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 } };
|
|
reducer(state, save);
|
|
|
|
// Reset clip
|
|
Action reset = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
|
|
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" } };
|
|
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 };
|
|
reducer(state, quit);
|
|
assert(state->running == false);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 29: MIDI clip initial state
|
|
// ============================================================
|
|
void test_midi_clip_initial_state(void) {
|
|
printf("Test 29: MIDI clip initial state... ");
|
|
AppState *state = create_test_state();
|
|
|
|
for (int i = 0; i < MAX_CLIPS; i++) {
|
|
assert(state->midi_clips[i].state == CLIP_EMPTY);
|
|
assert(state->midi_clips[i].event_count == 0);
|
|
assert(state->midi_clips[i].read_index == 0);
|
|
assert(state->midi_clips[i].events != NULL);
|
|
assert(state->midi_clips[i].max_events == MAX_MIDI_EVENTS);
|
|
}
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 30: MIDI clip trigger empty starts recording
|
|
// ============================================================
|
|
void test_midi_clip_trigger_empty_starts_recording(void) {
|
|
printf("Test 30: MIDI clip trigger empty starts recording... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
|
|
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
|
assert(state->midi_clips[0].event_count == 0);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 31: MIDI clip trigger recording starts looping
|
|
// ============================================================
|
|
void test_midi_clip_trigger_recording_starts_looping(void) {
|
|
printf("Test 31: MIDI clip trigger recording starts looping... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Start recording
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
|
|
|
// Simulate some recorded events
|
|
state->midi_clips[0].event_count = 10;
|
|
|
|
// Trigger again to stop recording and start looping
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
assert(state->midi_clips[0].event_count == 10);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 32: MIDI clip trigger looping stops it
|
|
// ============================================================
|
|
void test_midi_clip_trigger_looping_stops(void) {
|
|
printf("Test 32: MIDI clip trigger looping stops it... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a looping MIDI clip
|
|
state->midi_clips[0].state = CLIP_LOOPING;
|
|
state->midi_clips[0].event_count = 10;
|
|
state->midi_clips[0].read_index = 5;
|
|
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_STOPPED);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 33: MIDI clip trigger stopped resumes looping
|
|
// ============================================================
|
|
void test_midi_clip_trigger_stopped_resumes_looping(void) {
|
|
printf("Test 33: MIDI clip trigger stopped resumes looping... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a stopped MIDI clip
|
|
state->midi_clips[0].state = CLIP_STOPPED;
|
|
state->midi_clips[0].event_count = 10;
|
|
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 34: MIDI clip full cycle
|
|
// ============================================================
|
|
void test_midi_clip_full_cycle(void) {
|
|
printf("Test 34: MIDI clip full cycle... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
|
|
// Empty -> Recording
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
|
|
|
// Recording -> Looping
|
|
state->midi_clips[0].event_count = 20;
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
assert(state->midi_clips[0].event_count == 20);
|
|
|
|
// Looping -> Stopped
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_STOPPED);
|
|
|
|
// Stopped -> Looping
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 35: MIDI clip reset
|
|
// ============================================================
|
|
void test_midi_clip_reset(void) {
|
|
printf("Test 35: MIDI clip reset... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a MIDI clip with data
|
|
state->midi_clips[0].state = CLIP_LOOPING;
|
|
state->midi_clips[0].event_count = 20;
|
|
state->midi_clips[0].read_index = 10;
|
|
|
|
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
assert(state->midi_clips[0].state == CLIP_EMPTY);
|
|
assert(state->midi_clips[0].event_count == 0);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
// events pointer should still be valid
|
|
assert(state->midi_clips[0].events != NULL);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 36: MIDI clip multiple clips work independently
|
|
// ============================================================
|
|
void test_midi_clip_multiple_clips(void) {
|
|
printf("Test 36: MIDI clip multiple clips work independently... ");
|
|
AppState *state = create_test_state();
|
|
|
|
Action action0 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
Action action1 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 1 } };
|
|
Action action2 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 2 } };
|
|
|
|
// Clip 0: Empty -> Recording
|
|
reducer(state, action0);
|
|
assert(state->midi_clips[0].state == CLIP_RECORDING);
|
|
|
|
// Clip 1: Empty -> Recording
|
|
reducer(state, action1);
|
|
assert(state->midi_clips[1].state == CLIP_RECORDING);
|
|
|
|
// Clip 0: Recording -> Looping
|
|
state->midi_clips[0].event_count = 10;
|
|
reducer(state, action0);
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
assert(state->midi_clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
|
|
|
|
// Clip 2: Empty -> Recording
|
|
reducer(state, action2);
|
|
assert(state->midi_clips[2].state == CLIP_RECORDING);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 37: MIDI clip invalid index
|
|
// ============================================================
|
|
void test_midi_clip_invalid_index(void) {
|
|
printf("Test 37: MIDI clip invalid index... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// These should not crash
|
|
Action action_neg = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = -1 } };
|
|
Action action_max = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = MAX_CLIPS } };
|
|
Action reset_neg = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = -1 } };
|
|
Action reset_max = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = MAX_CLIPS } };
|
|
|
|
reducer(state, action_neg);
|
|
reducer(state, action_max);
|
|
reducer(state, reset_neg);
|
|
reducer(state, reset_max);
|
|
|
|
// Verify no state changes
|
|
assert(state->midi_clips[0].state == CLIP_EMPTY);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 38: MIDI clip show_midi_grid toggle
|
|
// ============================================================
|
|
void test_midi_clip_show_grid_toggle(void) {
|
|
printf("Test 38: MIDI clip show_midi_grid toggle... ");
|
|
AppState *state = create_test_state();
|
|
|
|
assert(state->show_midi_grid == false);
|
|
|
|
Action action_on = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = true } };
|
|
reducer(state, action_on);
|
|
assert(state->show_midi_grid == true);
|
|
|
|
Action action_off = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = false } };
|
|
reducer(state, action_off);
|
|
assert(state->show_midi_grid == false);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 39: MIDI clip events pointer persists after reset
|
|
// ============================================================
|
|
void test_midi_clip_events_persist_after_reset(void) {
|
|
printf("Test 39: MIDI clip events pointer persists after reset... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Get the original events pointer
|
|
MidiEvent *original_events = state->midi_clips[0].events;
|
|
assert(original_events != NULL);
|
|
|
|
// Set up some events
|
|
state->midi_clips[0].events[0].note = 60;
|
|
state->midi_clips[0].events[0].velocity = 100;
|
|
state->midi_clips[0].events[0].timestamp = 0;
|
|
state->midi_clips[0].event_count = 1;
|
|
state->midi_clips[0].state = CLIP_LOOPING;
|
|
|
|
// Reset the clip
|
|
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
|
|
// Events pointer should still be the same (not freed)
|
|
assert(state->midi_clips[0].events == original_events);
|
|
assert(state->midi_clips[0].state == CLIP_EMPTY);
|
|
assert(state->midi_clips[0].event_count == 0);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 40: MIDI clip trigger with events in buffer
|
|
// ============================================================
|
|
void test_midi_clip_trigger_with_events(void) {
|
|
printf("Test 40: MIDI clip trigger with events in buffer... ");
|
|
AppState *state = create_test_state();
|
|
|
|
// Set up a MIDI clip with events already recorded
|
|
state->midi_clips[0].events[0].note = 60;
|
|
state->midi_clips[0].events[0].velocity = 100;
|
|
state->midi_clips[0].events[0].timestamp = 0;
|
|
state->midi_clips[0].events[1].note = 64;
|
|
state->midi_clips[0].events[1].velocity = 80;
|
|
state->midi_clips[0].events[1].timestamp = 100;
|
|
state->midi_clips[0].event_count = 2;
|
|
state->midi_clips[0].state = CLIP_STOPPED;
|
|
|
|
// Trigger to resume looping
|
|
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
|
|
reducer(state, action);
|
|
|
|
assert(state->midi_clips[0].state == CLIP_LOOPING);
|
|
assert(state->midi_clips[0].event_count == 2);
|
|
assert(state->midi_clips[0].read_index == 0);
|
|
|
|
// Verify events are intact
|
|
assert(state->midi_clips[0].events[0].note == 60);
|
|
assert(state->midi_clips[0].events[1].note == 64);
|
|
|
|
destroy_test_state(state);
|
|
printf("PASSED\n");
|
|
}
|
|
|
|
// ============================================================
|
|
// Main
|
|
// ============================================================
|
|
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_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();
|
|
|
|
test_midi_clip_initial_state();
|
|
test_midi_clip_trigger_empty_starts_recording();
|
|
test_midi_clip_trigger_recording_starts_looping();
|
|
test_midi_clip_trigger_looping_stops();
|
|
test_midi_clip_trigger_stopped_resumes_looping();
|
|
test_midi_clip_full_cycle();
|
|
test_midi_clip_reset();
|
|
test_midi_clip_multiple_clips();
|
|
test_midi_clip_invalid_index();
|
|
test_midi_clip_show_grid_toggle();
|
|
test_midi_clip_events_persist_after_reset();
|
|
test_midi_clip_trigger_with_events();
|
|
|
|
printf("\nAll tests passed!\n");
|
|
exit(0);
|
|
return 0;
|
|
}
|