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:
Loic Coenen
2026-05-01 20:10:45 +00:00
parent b64b0cd418
commit be3582bc13
4 changed files with 702 additions and 8 deletions

259
engine.c
View File

@@ -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;
@@ -419,6 +486,16 @@ void engine_process_commands(Engine *engine) {
atomic_store(&engine->transport_bar_position, 0);
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);
}