feat: implement undo/redo system with history tracking and tests
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
259
engine.c
259
engine.c
@@ -333,6 +333,17 @@ void engine_process_commands(Engine *engine) {
|
||||
if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break;
|
||||
Clip *clip = &engine->clips[cmd.index];
|
||||
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_TRIGGER_CLIP;
|
||||
action.index = cmd.index;
|
||||
action.value = 0;
|
||||
action.previous_state = clip->state;
|
||||
action.previous_buffer_size = clip->buffer_size;
|
||||
action.previous_write_position = clip->write_position;
|
||||
action.previous_read_position = clip->read_position;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
switch (clip->state) {
|
||||
case CLIP_EMPTY:
|
||||
clip->state = CLIP_RECORDING;
|
||||
@@ -359,6 +370,14 @@ void engine_process_commands(Engine *engine) {
|
||||
|
||||
case CMD_TRIGGER_SCENE: {
|
||||
if (cmd.index < 0 || cmd.index >= MAX_SCENES) break;
|
||||
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_TRIGGER_SCENE;
|
||||
action.index = cmd.index;
|
||||
action.value = 0;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
int clip_idx = CLIP_INDEX(cmd.index, ch);
|
||||
Clip *clip = &engine->clips[clip_idx];
|
||||
@@ -391,6 +410,18 @@ void engine_process_commands(Engine *engine) {
|
||||
case CMD_RESET_CLIP: {
|
||||
if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break;
|
||||
Clip *clip = &engine->clips[cmd.index];
|
||||
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_RESET_CLIP;
|
||||
action.index = cmd.index;
|
||||
action.value = 0;
|
||||
action.previous_state = clip->state;
|
||||
action.previous_buffer_size = clip->buffer_size;
|
||||
action.previous_write_position = clip->write_position;
|
||||
action.previous_read_position = clip->read_position;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
clip->state = CLIP_EMPTY;
|
||||
clip->buffer_size = 0;
|
||||
clip->write_position = 0;
|
||||
@@ -399,15 +430,51 @@ void engine_process_commands(Engine *engine) {
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_SET_QUANTIZE_MODE:
|
||||
engine->quantize_mode = (QuantizeMode)cmd.index;
|
||||
break;
|
||||
case CMD_SET_QUANTIZE_MODE: {
|
||||
QuantizeMode new_mode = (QuantizeMode)cmd.index;
|
||||
|
||||
case CMD_SET_QUANTIZE_THRESHOLD:
|
||||
engine->quantize_threshold = cmd.value;
|
||||
break;
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_SET_QUANTIZE_MODE;
|
||||
action.index = (int)new_mode;
|
||||
action.value = 0;
|
||||
action.previous_quantize_mode = engine->quantize_mode;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
engine->quantize_mode = new_mode;
|
||||
atomic_store(&engine->quantize_mode_atomic, (int)new_mode);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_SET_QUANTIZE_THRESHOLD: {
|
||||
jack_nframes_t new_threshold = cmd.value;
|
||||
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_SET_QUANTIZE_THRESHOLD;
|
||||
action.index = 0;
|
||||
action.value = new_threshold;
|
||||
action.previous_quantize_threshold = engine->quantize_threshold;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
engine->quantize_threshold = new_threshold;
|
||||
atomic_store(&engine->quantize_threshold_atomic, new_threshold);
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_RESET_TRANSPORT: {
|
||||
// Record undo action
|
||||
UndoAction action;
|
||||
action.type = ACTION_RESET_TRANSPORT;
|
||||
action.index = 0;
|
||||
action.value = 0;
|
||||
action.previous_rolling = engine->transport.rolling;
|
||||
action.previous_clock_count = engine->transport.clock_count;
|
||||
action.previous_beat_position = engine->transport.beat_position;
|
||||
action.previous_bar_position = engine->transport.bar_position;
|
||||
action.previous_sample_position = engine->transport.sample_position;
|
||||
engine_push_undo_action(engine, &action);
|
||||
|
||||
case CMD_RESET_TRANSPORT:
|
||||
engine->transport.rolling = false;
|
||||
engine->transport.clock_count = 0;
|
||||
engine->transport.beat_position = 0;
|
||||
@@ -420,6 +487,16 @@ void engine_process_commands(Engine *engine) {
|
||||
atomic_store(&engine->transport_sample_position, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
case CMD_UNDO:
|
||||
engine_undo(engine);
|
||||
break;
|
||||
|
||||
case CMD_REDO:
|
||||
engine_redo(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
read++;
|
||||
}
|
||||
@@ -428,6 +505,159 @@ void engine_process_commands(Engine *engine) {
|
||||
atomic_store(&q->read_index, read);
|
||||
}
|
||||
|
||||
// Push an action to the undo history
|
||||
void engine_push_undo_action(Engine *engine, UndoAction *action) {
|
||||
if (!engine || !action) return;
|
||||
|
||||
UndoHistory *history = &engine->undo_history;
|
||||
|
||||
// If we've undone some actions, clear the redo history
|
||||
if (history->redo_index > history->undo_index) {
|
||||
history->redo_index = history->undo_index;
|
||||
}
|
||||
|
||||
// Add action at current undo position
|
||||
int slot = history->undo_index % MAX_UNDO_HISTORY;
|
||||
history->actions[slot] = *action;
|
||||
history->undo_index++;
|
||||
history->redo_index = history->undo_index;
|
||||
|
||||
if (history->count < MAX_UNDO_HISTORY) {
|
||||
history->count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Undo the last action
|
||||
void engine_undo(Engine *engine) {
|
||||
if (!engine) return;
|
||||
|
||||
UndoHistory *history = &engine->undo_history;
|
||||
if (history->undo_index <= 0) return; // Nothing to undo
|
||||
|
||||
int slot = (history->undo_index - 1) % MAX_UNDO_HISTORY;
|
||||
UndoAction *action = &history->actions[slot];
|
||||
|
||||
switch (action->type) {
|
||||
case ACTION_TRIGGER_CLIP: {
|
||||
int clip_idx = action->index;
|
||||
engine->clips[clip_idx].state = action->previous_state;
|
||||
engine->clips[clip_idx].buffer_size = action->previous_buffer_size;
|
||||
engine->clips[clip_idx].write_position = action->previous_write_position;
|
||||
engine->clips[clip_idx].read_position = action->previous_read_position;
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_TRIGGER_SCENE: {
|
||||
int scene_idx = action->index;
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
int clip_idx = CLIP_INDEX(scene_idx, ch);
|
||||
engine->clips[clip_idx].state = CLIP_EMPTY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_RESET_CLIP: {
|
||||
int clip_idx = action->index;
|
||||
engine->clips[clip_idx].state = action->previous_state;
|
||||
engine->clips[clip_idx].buffer_size = action->previous_buffer_size;
|
||||
engine->clips[clip_idx].write_position = action->previous_write_position;
|
||||
engine->clips[clip_idx].read_position = action->previous_read_position;
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_SET_QUANTIZE_MODE: {
|
||||
engine->quantize_mode = action->previous_quantize_mode;
|
||||
atomic_store(&engine->quantize_mode_atomic, (int)action->previous_quantize_mode);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_SET_QUANTIZE_THRESHOLD: {
|
||||
engine->quantize_threshold = action->previous_quantize_threshold;
|
||||
atomic_store(&engine->quantize_threshold_atomic, action->previous_quantize_threshold);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_RESET_TRANSPORT: {
|
||||
engine->transport.rolling = action->previous_rolling;
|
||||
engine->transport.clock_count = action->previous_clock_count;
|
||||
engine->transport.beat_position = action->previous_beat_position;
|
||||
engine->transport.bar_position = action->previous_bar_position;
|
||||
engine->transport.sample_position = action->previous_sample_position;
|
||||
atomic_store(&engine->transport_rolling, action->previous_rolling ? 1 : 0);
|
||||
atomic_store(&engine->transport_clock_count, action->previous_clock_count);
|
||||
atomic_store(&engine->transport_beat_position, action->previous_beat_position);
|
||||
atomic_store(&engine->transport_bar_position, action->previous_bar_position);
|
||||
atomic_store(&engine->transport_sample_position, action->previous_sample_position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
history->undo_index--;
|
||||
}
|
||||
|
||||
// Redo the last undone action
|
||||
void engine_redo(Engine *engine) {
|
||||
if (!engine) return;
|
||||
|
||||
UndoHistory *history = &engine->undo_history;
|
||||
if (history->redo_index <= history->undo_index) return; // Nothing to redo
|
||||
|
||||
int slot = history->undo_index % MAX_UNDO_HISTORY;
|
||||
UndoAction *action = &history->actions[slot];
|
||||
|
||||
switch (action->type) {
|
||||
case ACTION_TRIGGER_CLIP: {
|
||||
int clip_idx = action->index;
|
||||
// Re-apply the trigger
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_TRIGGER_SCENE: {
|
||||
int scene_idx = action->index;
|
||||
engine_trigger_scene(engine, scene_idx);
|
||||
engine_process_commands(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_RESET_CLIP: {
|
||||
int clip_idx = action->index;
|
||||
engine_reset_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_SET_QUANTIZE_MODE: {
|
||||
engine->quantize_mode = (QuantizeMode)action->index;
|
||||
atomic_store(&engine->quantize_mode_atomic, action->index);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_SET_QUANTIZE_THRESHOLD: {
|
||||
engine->quantize_threshold = action->value;
|
||||
atomic_store(&engine->quantize_threshold_atomic, action->value);
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTION_RESET_TRANSPORT: {
|
||||
engine->transport.rolling = true;
|
||||
engine->transport.clock_count = 0;
|
||||
engine->transport.beat_position = 0;
|
||||
engine->transport.bar_position = 0;
|
||||
engine->transport.sample_position = 0;
|
||||
atomic_store(&engine->transport_rolling, 1);
|
||||
atomic_store(&engine->transport_clock_count, 0);
|
||||
atomic_store(&engine->transport_beat_position, 0);
|
||||
atomic_store(&engine->transport_bar_position, 0);
|
||||
atomic_store(&engine->transport_sample_position, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
history->undo_index++;
|
||||
}
|
||||
|
||||
int engine_init(Engine *engine, const char *client_name) {
|
||||
if (!engine || !client_name) return -1;
|
||||
|
||||
@@ -438,6 +668,11 @@ int engine_init(Engine *engine, const char *client_name) {
|
||||
engine->quantize_threshold = 0;
|
||||
engine->queued_triggers = NULL;
|
||||
|
||||
// Initialize undo history
|
||||
engine->undo_history.undo_index = 0;
|
||||
engine->undo_history.redo_index = 0;
|
||||
engine->undo_history.count = 0;
|
||||
|
||||
// Initialize command queue
|
||||
command_queue_init(&engine->command_queue);
|
||||
|
||||
@@ -647,3 +882,13 @@ const char* quantize_mode_to_string(QuantizeMode mode) {
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void engine_undo_action(Engine *engine) {
|
||||
if (!engine) return;
|
||||
engine_submit_command(engine, CMD_UNDO, 0, 0);
|
||||
}
|
||||
|
||||
void engine_redo_action(Engine *engine) {
|
||||
if (!engine) return;
|
||||
engine_submit_command(engine, CMD_REDO, 0, 0);
|
||||
}
|
||||
|
||||
52
engine.h
52
engine.h
@@ -56,9 +56,49 @@ typedef enum {
|
||||
CMD_RESET_CLIP,
|
||||
CMD_SET_QUANTIZE_MODE,
|
||||
CMD_SET_QUANTIZE_THRESHOLD,
|
||||
CMD_RESET_TRANSPORT
|
||||
CMD_RESET_TRANSPORT,
|
||||
CMD_UNDO,
|
||||
CMD_REDO
|
||||
} CommandType;
|
||||
|
||||
// Undo/Redo action types
|
||||
typedef enum {
|
||||
ACTION_TRIGGER_CLIP,
|
||||
ACTION_TRIGGER_SCENE,
|
||||
ACTION_RESET_CLIP,
|
||||
ACTION_SET_QUANTIZE_MODE,
|
||||
ACTION_SET_QUANTIZE_THRESHOLD,
|
||||
ACTION_RESET_TRANSPORT
|
||||
} ActionType;
|
||||
|
||||
// Undo/Redo action record
|
||||
typedef struct {
|
||||
ActionType type;
|
||||
int index; // clip_index, scene_index, or mode value
|
||||
jack_nframes_t value; // threshold value or other numeric param
|
||||
ClipState previous_state; // For clip state changes
|
||||
size_t previous_buffer_size;
|
||||
size_t previous_write_position;
|
||||
size_t previous_read_position;
|
||||
bool previous_rolling;
|
||||
uint32_t previous_clock_count;
|
||||
uint32_t previous_beat_position;
|
||||
uint32_t previous_bar_position;
|
||||
uint32_t previous_sample_position;
|
||||
QuantizeMode previous_quantize_mode;
|
||||
jack_nframes_t previous_quantize_threshold;
|
||||
} UndoAction;
|
||||
|
||||
// Undo/Redo history
|
||||
#define MAX_UNDO_HISTORY 256
|
||||
|
||||
typedef struct {
|
||||
UndoAction actions[MAX_UNDO_HISTORY];
|
||||
int undo_index; // Points to next action to undo
|
||||
int redo_index; // Points to next action to redo
|
||||
int count; // Total actions in history
|
||||
} UndoHistory;
|
||||
|
||||
typedef struct {
|
||||
CommandType type;
|
||||
int index; // clip_index, scene_index, or mode value
|
||||
@@ -115,6 +155,9 @@ typedef struct {
|
||||
atomic_uint quantize_threshold_atomic;
|
||||
|
||||
bool running;
|
||||
|
||||
// Undo/Redo
|
||||
UndoHistory undo_history;
|
||||
} Engine;
|
||||
|
||||
// Engine lifecycle
|
||||
@@ -150,4 +193,11 @@ const char* clip_state_to_string(ClipState state);
|
||||
uint8_t clip_state_to_velocity(ClipState state);
|
||||
const char* quantize_mode_to_string(QuantizeMode mode);
|
||||
|
||||
// Undo/Redo
|
||||
void engine_undo(Engine *engine);
|
||||
void engine_redo(Engine *engine);
|
||||
void engine_push_undo_action(Engine *engine, UndoAction *action);
|
||||
void engine_undo_action(Engine *engine);
|
||||
void engine_redo_action(Engine *engine);
|
||||
|
||||
#endif // ENGINE_H
|
||||
|
||||
385
test_tui.c
385
test_tui.c
@@ -1211,6 +1211,379 @@ void test_remark_existing_mark(void) {
|
||||
printf("PASSED\n");
|
||||
}
|
||||
|
||||
// Test 53: Undo single clip trigger
|
||||
void test_undo_single_trigger(void) {
|
||||
printf("Test 53: Undo single clip trigger... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Start with empty clip
|
||||
int clip_idx = 10;
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
|
||||
// Trigger clip (empty -> recording)
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
|
||||
// Undo: should go back to empty
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 54: Undo multiple clip triggers
|
||||
void test_undo_multiple_triggers(void) {
|
||||
printf("Test 54: Undo multiple clip triggers... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip1 = 10, clip2 = 11, clip3 = 12;
|
||||
|
||||
// Trigger three clips
|
||||
engine_trigger_clip(engine, clip1);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
|
||||
engine_trigger_clip(engine, clip2);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip2].state == CLIP_RECORDING);
|
||||
|
||||
engine_trigger_clip(engine, clip3);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip3].state == CLIP_RECORDING);
|
||||
|
||||
// Undo last action: clip3 should go back to empty
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip3].state == CLIP_EMPTY);
|
||||
assert(engine->clips[clip2].state == CLIP_RECORDING);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
|
||||
// Undo again: clip2 should go back to empty
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip2].state == CLIP_EMPTY);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 55: Redo single undo
|
||||
void test_redo_single_undo(void) {
|
||||
printf("Test 55: Redo single undo... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip_idx = 10;
|
||||
|
||||
// Trigger clip
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
|
||||
// Undo
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
|
||||
// Redo: should go back to recording
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 56: Redo after multiple undos
|
||||
void test_redo_multiple_undos(void) {
|
||||
printf("Test 56: Redo after multiple undos... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip1 = 10, clip2 = 11;
|
||||
|
||||
// Trigger two clips
|
||||
engine_trigger_clip(engine, clip1);
|
||||
engine_process_commands(engine);
|
||||
engine_trigger_clip(engine, clip2);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
assert(engine->clips[clip2].state == CLIP_RECORDING);
|
||||
|
||||
// Undo twice
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_EMPTY);
|
||||
assert(engine->clips[clip2].state == CLIP_EMPTY);
|
||||
|
||||
// Redo twice
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
assert(engine->clips[clip2].state == CLIP_EMPTY);
|
||||
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
assert(engine->clips[clip2].state == CLIP_RECORDING);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 57: Undo scene trigger
|
||||
void test_undo_scene_trigger(void) {
|
||||
printf("Test 57: Undo scene trigger... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int scene_idx = 3;
|
||||
|
||||
// Trigger scene
|
||||
engine_trigger_scene(engine, scene_idx);
|
||||
engine_process_commands(engine);
|
||||
|
||||
// All clips in scene should be recording
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
int clip_idx = CLIP_INDEX(scene_idx, ch);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
}
|
||||
|
||||
// Undo: all clips should go back to empty
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
int clip_idx = CLIP_INDEX(scene_idx, ch);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
}
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped)
|
||||
void test_undo_clip_state_cycle(void) {
|
||||
printf("Test 58: Undo clip state cycle... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip_idx = 10;
|
||||
|
||||
// Cycle through all states
|
||||
engine_trigger_clip(engine, clip_idx); // empty -> recording
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
|
||||
engine_trigger_clip(engine, clip_idx); // recording -> looping
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
|
||||
|
||||
engine_trigger_clip(engine, clip_idx); // looping -> stopped
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
|
||||
|
||||
// Undo three times to go back to empty
|
||||
engine_undo_action(engine); // stopped -> looping
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
|
||||
|
||||
engine_undo_action(engine); // looping -> recording
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_RECORDING);
|
||||
|
||||
engine_undo_action(engine); // recording -> empty
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 59: Undo after new action clears redo history
|
||||
void test_undo_clears_redo_on_new_action(void) {
|
||||
printf("Test 59: Undo after new action clears redo history... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip1 = 10, clip2 = 11;
|
||||
|
||||
// Trigger clip1
|
||||
engine_trigger_clip(engine, clip1);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
|
||||
// Undo
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_EMPTY);
|
||||
|
||||
// Redo should work
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_RECORDING);
|
||||
|
||||
// Undo again
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_EMPTY);
|
||||
|
||||
// Now do a new action (trigger clip2)
|
||||
engine_trigger_clip(engine, clip2);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip2].state == CLIP_RECORDING);
|
||||
|
||||
// Redo should NOT work now (redo history cleared)
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip1].state == CLIP_EMPTY); // Should still be empty
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 60: Undo reset clip
|
||||
void test_undo_reset_clip(void) {
|
||||
printf("Test 60: Undo reset clip... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip_idx = 10;
|
||||
|
||||
// Set up a looping clip with data
|
||||
engine->clips[clip_idx].state = CLIP_LOOPING;
|
||||
engine->clips[clip_idx].buffer_size = 100;
|
||||
engine->clips[clip_idx].write_position = 100;
|
||||
engine->clips[clip_idx].read_position = 50;
|
||||
|
||||
// Reset the clip
|
||||
engine_reset_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
assert(engine->clips[clip_idx].buffer_size == 0);
|
||||
|
||||
// Undo: should restore clip to previous state
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_LOOPING);
|
||||
assert(engine->clips[clip_idx].buffer_size == 100);
|
||||
assert(engine->clips[clip_idx].write_position == 100);
|
||||
assert(engine->clips[clip_idx].read_position == 50);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 61: Undo/redo with quantize mode change
|
||||
void test_undo_quantize_change(void) {
|
||||
printf("Test 61: Undo quantize mode change... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
assert(engine->quantize_mode == QUANTIZE_OFF);
|
||||
|
||||
// Change quantize mode
|
||||
engine_set_quantize_mode(engine, QUANTIZE_BEAT);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_mode == QUANTIZE_BEAT);
|
||||
|
||||
// Undo: should go back to OFF
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_mode == QUANTIZE_OFF);
|
||||
|
||||
// Redo: should go back to BEAT
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_mode == QUANTIZE_BEAT);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 62: Undo/redo with threshold change
|
||||
void test_undo_threshold_change(void) {
|
||||
printf("Test 62: Undo threshold change... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
assert(engine->quantize_threshold == 0);
|
||||
|
||||
// Change threshold
|
||||
engine_set_quantize_threshold(engine, 1000);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_threshold == 1000);
|
||||
|
||||
// Undo: should go back to 0
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_threshold == 0);
|
||||
|
||||
// Redo: should go back to 1000
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->quantize_threshold == 1000);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 63: Undo/redo with transport reset
|
||||
void test_undo_transport_reset(void) {
|
||||
printf("Test 63: Undo transport reset... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
// Set up transport state
|
||||
engine->transport.rolling = true;
|
||||
engine->transport.clock_count = 100;
|
||||
engine->transport.beat_position = 2;
|
||||
engine->transport.bar_position = 5;
|
||||
engine->transport.sample_position = 10000;
|
||||
|
||||
// Reset transport
|
||||
engine_reset_transport(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->transport.rolling == false);
|
||||
assert(engine->transport.clock_count == 0);
|
||||
|
||||
// Undo: should restore transport state
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->transport.rolling == true);
|
||||
assert(engine->transport.clock_count == 100);
|
||||
assert(engine->transport.beat_position == 2);
|
||||
assert(engine->transport.bar_position == 5);
|
||||
assert(engine->transport.sample_position == 10000);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
// Test 64: Undo/redo with paste operation
|
||||
void test_undo_paste(void) {
|
||||
printf("Test 64: Undo paste operation... ");
|
||||
Engine *engine = create_test_engine();
|
||||
|
||||
int clip_idx = 27;
|
||||
|
||||
// Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_trigger_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
|
||||
|
||||
// Undo: should go back to empty
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_EMPTY);
|
||||
|
||||
// Redo: should go back to stopped
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
assert(engine->clips[clip_idx].state == CLIP_STOPPED);
|
||||
|
||||
printf("PASSED\n");
|
||||
destroy_test_engine(engine);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Running TUI tests...\n\n");
|
||||
|
||||
@@ -1266,6 +1639,18 @@ int main(void) {
|
||||
test_visual_yank_then_escape();
|
||||
test_multiple_marks();
|
||||
test_remark_existing_mark();
|
||||
test_undo_single_trigger();
|
||||
test_undo_multiple_triggers();
|
||||
test_redo_single_undo();
|
||||
test_redo_multiple_undos();
|
||||
test_undo_scene_trigger();
|
||||
test_undo_clip_state_cycle();
|
||||
test_undo_clears_redo_on_new_action();
|
||||
test_undo_reset_clip();
|
||||
test_undo_quantize_change();
|
||||
test_undo_threshold_change();
|
||||
test_undo_transport_reset();
|
||||
test_undo_paste();
|
||||
|
||||
printf("\nAll TUI tests passed!\n");
|
||||
return 0;
|
||||
|
||||
14
tui.c
14
tui.c
@@ -735,6 +735,20 @@ void tui_run(Engine *engine) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'u': {
|
||||
// Undo
|
||||
engine_undo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
case 18: { // Ctrl+R (18 = 0x12)
|
||||
// Redo
|
||||
engine_redo_action(engine);
|
||||
engine_process_commands(engine);
|
||||
break;
|
||||
}
|
||||
|
||||
case 27: // Escape key
|
||||
case 'Q':
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user