diff --git a/dispatcher.c b/dispatcher.c index af48359..82607de 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -116,21 +116,25 @@ AppState dispatcher_get_state(void) { // Reducer implementation // ============================================================ +static void save_undo_state(AppState *state, int clip_index) { + int undo_idx = state->undo.undo_index % MAX_UNDO_HISTORY; + state->undo.prev_clip_states[undo_idx] = state->clips[clip_index].state; + state->undo.prev_clip_indices[undo_idx] = clip_index; + state->undo.prev_buffer_sizes[undo_idx] = state->clips[clip_index].buffer_size; + state->undo.prev_write_positions[undo_idx] = state->clips[clip_index].write_position; + state->undo.prev_read_positions[undo_idx] = state->clips[clip_index].read_position; + state->undo.batch_sizes[undo_idx] = 0; // Will be set by caller + state->undo.undo_index++; + state->undo.redo_index = state->undo.undo_index; + if (state->undo.count < MAX_UNDO_HISTORY) state->undo.count++; +} + static AppState clip_trigger(AppState state, int clip_index) { if (clip_index < 0 || clip_index >= MAX_CLIPS) return state; Clip *clip = &state.clips[clip_index]; - // Save undo info - int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; - state.undo.prev_clip_states[undo_idx] = clip->state; - state.undo.prev_clip_indices[undo_idx] = clip_index; - state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size; - state.undo.prev_write_positions[undo_idx] = clip->write_position; - state.undo.prev_read_positions[undo_idx] = clip->read_position; - state.undo.undo_index++; - state.undo.redo_index = state.undo.undo_index; - if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++; + // Do NOT save undo here - caller will do it switch (clip->state) { case CLIP_EMPTY: @@ -160,6 +164,20 @@ static AppState clip_trigger(AppState state, int clip_index) { static AppState scene_trigger(AppState state, int scene_index) { if (scene_index < 0 || scene_index >= MAX_SCENES) return state; + // Save undo info for all clips in the scene as a batch + int batch_start = state.undo.undo_index; + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_index, ch); + save_undo_state(&state, clip_idx); + } + // Mark all entries in this batch with the batch size + int batch_end = state.undo.undo_index; + for (int i = batch_start; i < batch_end; i++) { + int idx = i % MAX_UNDO_HISTORY; + state.undo.batch_sizes[idx] = batch_end - batch_start; + } + + // Now apply the changes for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = CLIP_INDEX(scene_index, ch); state = clip_trigger(state, clip_idx); @@ -173,16 +191,7 @@ static AppState clip_reset(AppState state, int clip_index) { Clip *clip = &state.clips[clip_index]; - // Save undo info - int undo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; - state.undo.prev_clip_states[undo_idx] = clip->state; - state.undo.prev_clip_indices[undo_idx] = clip_index; - state.undo.prev_buffer_sizes[undo_idx] = clip->buffer_size; - state.undo.prev_write_positions[undo_idx] = clip->write_position; - state.undo.prev_read_positions[undo_idx] = clip->read_position; - state.undo.undo_index++; - state.undo.redo_index = state.undo.undo_index; - if (state.undo.count < MAX_UNDO_HISTORY) state.undo.count++; + save_undo_state(&state, clip_index); clip->state = CLIP_EMPTY; clip->buffer_size = 0; @@ -196,60 +205,79 @@ static AppState clip_reset(AppState state, int clip_index) { static AppState undo_action(AppState state) { if (state.undo.undo_index <= 0) return state; + // Get the batch size for the current undo entry int undo_idx = (state.undo.undo_index - 1) % MAX_UNDO_HISTORY; - int clip_idx = state.undo.prev_clip_indices[undo_idx]; + int batch_size = state.undo.batch_sizes[undo_idx]; + if (batch_size == 0) batch_size = 1; // Single clip operation - if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { - Clip *clip = &state.clips[clip_idx]; - clip->state = state.undo.prev_clip_states[undo_idx]; - clip->buffer_size = state.undo.prev_buffer_sizes[undo_idx]; - clip->write_position = state.undo.prev_write_positions[undo_idx]; - clip->read_position = state.undo.prev_read_positions[undo_idx]; + // Undo all clips in the batch + for (int i = 0; i < batch_size; i++) { + int current_idx = (state.undo.undo_index - 1 - i) % MAX_UNDO_HISTORY; + int clip_idx = state.undo.prev_clip_indices[current_idx]; + + if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { + Clip *clip = &state.clips[clip_idx]; + clip->state = state.undo.prev_clip_states[current_idx]; + clip->buffer_size = state.undo.prev_buffer_sizes[current_idx]; + clip->write_position = state.undo.prev_write_positions[current_idx]; + clip->read_position = state.undo.prev_read_positions[current_idx]; + } } - state.undo.undo_index--; + state.undo.undo_index -= batch_size; return state; } static AppState redo_action(AppState state) { if (state.undo.redo_index <= state.undo.undo_index) return state; + // Get the batch size for the next redo entry int redo_idx = state.undo.undo_index % MAX_UNDO_HISTORY; - int clip_idx = state.undo.prev_clip_indices[redo_idx]; + int batch_size = state.undo.batch_sizes[redo_idx]; + if (batch_size == 0) batch_size = 1; - if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { - Clip *clip = &state.clips[clip_idx]; - switch (clip->state) { - case CLIP_EMPTY: - clip->state = CLIP_RECORDING; - clip->write_position = 0; - clip->buffer_size = 0; - clip->read_position = 0; - break; - case CLIP_RECORDING: - clip->state = CLIP_LOOPING; - clip->buffer_size = clip->write_position; - clip->read_position = 0; - break; - case CLIP_LOOPING: - clip->state = CLIP_STOPPED; - clip->read_position = 0; - break; - case CLIP_STOPPED: - clip->state = CLIP_LOOPING; - clip->read_position = 0; - break; + // Redo all clips in the batch + for (int i = 0; i < batch_size; i++) { + int current_idx = (state.undo.undo_index + i) % MAX_UNDO_HISTORY; + int clip_idx = state.undo.prev_clip_indices[current_idx]; + + if (clip_idx >= 0 && clip_idx < MAX_CLIPS) { + Clip *clip = &state.clips[clip_idx]; + switch (clip->state) { + case CLIP_EMPTY: + clip->state = CLIP_RECORDING; + clip->write_position = 0; + clip->buffer_size = 0; + clip->read_position = 0; + break; + case CLIP_RECORDING: + clip->state = CLIP_LOOPING; + clip->buffer_size = clip->write_position; + clip->read_position = 0; + break; + case CLIP_LOOPING: + clip->state = CLIP_STOPPED; + clip->read_position = 0; + break; + case CLIP_STOPPED: + clip->state = CLIP_LOOPING; + clip->read_position = 0; + break; + } } } - state.undo.undo_index++; + state.undo.undo_index += batch_size; return state; } AppState reducer(AppState state, Action action) { switch (action.type) { - case ACTION_TRIGGER_CLIP: - return clip_trigger(state, action.data.trigger_clip.clip_index); + case ACTION_TRIGGER_CLIP: { + int clip_idx = action.data.trigger_clip.clip_index; + save_undo_state(&state, clip_idx); + return clip_trigger(state, clip_idx); + } case ACTION_TRIGGER_SCENE: return scene_trigger(state, action.data.trigger_scene.scene_index); diff --git a/dispatcher.h b/dispatcher.h index 80e9875..2b3f2fa 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -81,6 +81,8 @@ typedef struct { size_t prev_buffer_sizes[MAX_UNDO_HISTORY]; size_t prev_write_positions[MAX_UNDO_HISTORY]; size_t prev_read_positions[MAX_UNDO_HISTORY]; + int batch_sizes[MAX_UNDO_HISTORY]; // NEW: track batch sizes + int current_batch_size; // NEW: current batch being recorded } undo; // Carla host diff --git a/test_tui.c b/test_tui.c index 990e777..ae9369d 100644 --- a/test_tui.c +++ b/test_tui.c @@ -40,8 +40,10 @@ static AppState create_test_state(void) { state.undo.undo_index = 0; state.undo.redo_index = 0; state.undo.count = 0; + state.undo.current_batch_size = 0; for (int i = 0; i < MAX_UNDO_HISTORY; i++) { state.undo.prev_clip_indices[i] = -1; + state.undo.batch_sizes[i] = 0; } // JACK info