diff --git a/dispatcher.c b/dispatcher.c index ab21e68..ad830f6 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -168,12 +168,12 @@ 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; + if (scene_index < 0 || scene_index >= MAX_SCENES * 8) return state; // 8 grids // 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); + int clip_idx = scene_index * MAX_CHANNELS + ch; save_undo_state(&state, clip_idx); } // Mark all entries in this batch with the batch size @@ -185,7 +185,7 @@ static AppState scene_trigger(AppState state, int scene_index) { // Now apply the changes for (int ch = 0; ch < MAX_CHANNELS; ch++) { - int clip_idx = CLIP_INDEX(scene_index, ch); + int clip_idx = scene_index * MAX_CHANNELS + ch; state = clip_trigger(state, clip_idx); } @@ -395,6 +395,7 @@ AppState reducer(AppState state, Action action) { case ACTION_MIDI_NOTE_ON: { int clip_index = action.data.midi_note_on.note % MAX_CLIPS; + save_undo_state(&state, clip_index); return clip_trigger(state, clip_index); } @@ -466,7 +467,11 @@ AppState reducer(AppState state, Action action) { clip->buffer_size = 0; clip->write_position = 0; clip->read_position = 0; - if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); + if (clip->buffer) { + memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); + } else { + clip->buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + } } // Reset Carla host diff --git a/dispatcher.h b/dispatcher.h index 9383dee..f336c09 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -14,7 +14,7 @@ #define MAX_SCENES 8 #define MAX_CHANNELS 8 -#define MAX_CLIPS (MAX_SCENES * MAX_CHANNELS) // 64 +#define MAX_CLIPS 512 // 8 grids * 8 scenes * 8 channels #define MAX_BUFFER_SIZE 441000 // 10 seconds at 44.1kHz #define MAX_UNDO_HISTORY 256 diff --git a/engine.c b/engine.c index f133b31..3e6863e 100644 --- a/engine.c +++ b/engine.c @@ -95,18 +95,21 @@ static int process_callback(jack_nframes_t nframes, void *arg) { rack_in[i] = 0.0f; for (int s = 0; s < MAX_SCENES; s++) { - int clip_idx = CLIP_INDEX(s, ch); - Clip *clip = &state.clips[clip_idx]; - - if (clip->state == CLIP_RECORDING) { - if (clip->write_position < MAX_BUFFER_SIZE) { - clip->buffer[clip->write_position++] = audio_in[ch][i]; + // Iterate over all grids for this scene and channel + for (int g = 0; g < 8; g++) { + int clip_idx = g * GRID_ROWS * GRID_COLS + s * GRID_COLS + ch; + Clip *clip = &state.clips[clip_idx]; + + if (clip->state == CLIP_RECORDING) { + if (clip->write_position < MAX_BUFFER_SIZE) { + clip->buffer[clip->write_position++] = audio_in[ch][i]; + } + } + + if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) { + rack_in[i] += clip->buffer[clip->read_position]; + clip->read_position = (clip->read_position + 1) % clip->buffer_size; } - } - - if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) { - rack_in[i] += clip->buffer[clip->read_position]; - clip->read_position = (clip->read_position + 1) % clip->buffer_size; } } } diff --git a/main.c b/main.c index f418041..392ef63 100644 --- a/main.c +++ b/main.c @@ -84,6 +84,14 @@ int main(int argc, char *argv[]) { for (int i = 0; i < MAX_CLIPS; i++) { initial_state.clips[i].state = CLIP_EMPTY; initial_state.clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + if (!initial_state.clips[i].buffer) { + fprintf(stderr, "Failed to allocate buffer for clip %d\n", i); + // Cleanup previously allocated buffers + for (int j = 0; j < i; j++) { + free(initial_state.clips[j].buffer); + } + return 1; + } initial_state.clips[i].buffer_size = 0; initial_state.clips[i].write_position = 0; initial_state.clips[i].read_position = 0; diff --git a/tui.c b/tui.c index 727d48f..781a180 100644 --- a/tui.c +++ b/tui.c @@ -31,6 +31,10 @@ static int selected_row = 0; static int selected_col = 0; static bool show_help = false; +// Grid of grids state +static int selected_grid = 0; // Which grid we're viewing (0-7) +static bool zoom_mode = false; // Whether we're in zoom mode + // View modes typedef enum { VIEW_GRID, @@ -95,8 +99,8 @@ static int state_to_color(ClipState state) { } // Get clip index from grid position -static int grid_to_clip_index(int row, int col) { - return row * GRID_COLS + col; +static int grid_to_clip_index(int grid, int row, int col) { + return grid * GRID_ROWS * GRID_COLS + row * GRID_COLS + col; } // Check if a cell is in the visual selection @@ -131,7 +135,7 @@ static int* get_selected_clips(int *count) { int idx = 0; for (int r = min_row; r <= max_row; r++) { for (int c = min_col; c <= max_col; c++) { - clips[idx++] = grid_to_clip_index(r, c); + clips[idx++] = grid_to_clip_index(selected_grid, r, c); } } @@ -163,20 +167,27 @@ static void yank_clips(int *clip_indices, int count) { static void paste_clips(void) { if (!yank_buffer.clip_indices || yank_buffer.count == 0) return; - int first_yanked_row = yank_buffer.clip_indices[0] / GRID_COLS; - int first_yanked_col = yank_buffer.clip_indices[0] % GRID_COLS; + int first_yanked_grid = yank_buffer.clip_indices[0] / (GRID_ROWS * GRID_COLS); + int first_yanked_row = (yank_buffer.clip_indices[0] % (GRID_ROWS * GRID_COLS)) / GRID_COLS; + int first_yanked_col = (yank_buffer.clip_indices[0] % (GRID_ROWS * GRID_COLS)) % GRID_COLS; + int grid_offset = selected_grid - first_yanked_grid; int row_offset = selected_row - first_yanked_row; int col_offset = selected_col - first_yanked_col; for (int i = 0; i < yank_buffer.count; i++) { - int orig_row = yank_buffer.clip_indices[i] / GRID_COLS; - int orig_col = yank_buffer.clip_indices[i] % GRID_COLS; + int orig_grid = yank_buffer.clip_indices[i] / (GRID_ROWS * GRID_COLS); + int remainder = yank_buffer.clip_indices[i] % (GRID_ROWS * GRID_COLS); + int orig_row = remainder / GRID_COLS; + int orig_col = remainder % GRID_COLS; + int new_grid = orig_grid + grid_offset; int new_row = orig_row + row_offset; int new_col = orig_col + col_offset; - if (new_row >= 0 && new_row < GRID_ROWS && new_col >= 0 && new_col < GRID_COLS) { - int new_clip_idx = grid_to_clip_index(new_row, new_col); + if (new_grid >= 0 && new_grid < NUM_GRIDS && + new_row >= 0 && new_row < GRID_ROWS && + new_col >= 0 && new_col < GRID_COLS) { + int new_clip_idx = grid_to_clip_index(new_grid, new_row, new_col); // Trigger three times to cycle: empty -> recording -> looping -> stopped for (int j = 0; j < 3; j++) { Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = new_clip_idx } }; @@ -190,7 +201,7 @@ static void paste_clips(void) { static void set_mark(char mark_char) { if (mark_char >= 'a' && mark_char <= 'z') { int idx = mark_char - 'a'; - marks[idx] = grid_to_clip_index(selected_row, selected_col); + marks[idx] = grid_to_clip_index(selected_grid, selected_row, selected_col); } } @@ -199,9 +210,11 @@ static void go_to_mark(char mark_char) { if (mark_char >= 'a' && mark_char <= 'z') { int idx = mark_char - 'a'; int clip_idx = marks[idx]; - if (clip_idx >= 0 && clip_idx < GRID_ROWS * GRID_COLS) { - selected_row = clip_idx / GRID_COLS; - selected_col = clip_idx % GRID_COLS; + if (clip_idx >= 0 && clip_idx < NUM_GRIDS * GRID_ROWS * GRID_COLS) { + selected_grid = clip_idx / (GRID_ROWS * GRID_COLS); + int remainder = clip_idx % (GRID_ROWS * GRID_COLS); + selected_row = remainder / GRID_COLS; + selected_col = remainder % GRID_COLS; } } } @@ -209,7 +222,7 @@ static void go_to_mark(char mark_char) { // Play next scene (next row) static void play_next_scene(void) { int next_row = (selected_row + 1) % GRID_ROWS; - Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = next_row } }; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = next_row + selected_grid * GRID_ROWS } }; g_dispatch(action); selected_row = next_row; } @@ -217,7 +230,7 @@ static void play_next_scene(void) { // Play previous scene (previous row) static void play_prev_scene(void) { int prev_row = (selected_row - 1 + GRID_ROWS) % GRID_ROWS; - Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = prev_row } }; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = prev_row + selected_grid * GRID_ROWS } }; g_dispatch(action); selected_row = prev_row; } @@ -582,12 +595,12 @@ static bool handle_rack_view(int ch) { } // Draw a single cell -static void draw_cell(int row, int col, bool selected) { - int clip_idx = grid_to_clip_index(row, col); +static void draw_cell(int grid, int row, int col, bool selected) { + int clip_idx = grid_to_clip_index(grid, row, col); AppState state = dispatcher_get_state(); Clip *clip = &state.clips[clip_idx]; - int y = row * CELL_HEIGHT + 1; + int y = row * CELL_HEIGHT + 3; // Offset by 2 for grid selector int x = col * CELL_WIDTH + 1; int color = state_to_color(clip->state); @@ -624,27 +637,77 @@ static void draw_cell(int row, int col, bool selected) { static void draw_grid(void) { clear(); + if (zoom_mode) { + // Draw grid selector overview + attron(A_BOLD); + mvprintw(0, 0, "JACK Looper - Grid Selector (h/j/k/l navigate, Enter select, z exit)"); + attroff(A_BOLD); + + // Draw mini grids + for (int g = 0; g < NUM_GRIDS; g++) { + int gx = (g % 4) * 20; + int gy = (g / 4) * 10 + 2; + + if (g == selected_grid) { + attron(A_REVERSE); + } + mvprintw(gy, gx, "Grid %d", g); + if (g == selected_grid) { + attroff(A_REVERSE); + } + + // Draw mini representation (4x4 sampling of the 8x8 grid) + for (int r = 0; r < 4; r++) { + for (int c = 0; c < 4; c++) { + int clip_idx = grid_to_clip_index(g, r * 2, c * 2); + AppState state = dispatcher_get_state(); + Clip *clip = &state.clips[clip_idx]; + int color = state_to_color(clip->state); + attron(COLOR_PAIR(color)); + mvaddch(gy + 1 + r, gx + 1 + c, ' '); + attroff(COLOR_PAIR(color)); + } + } + } + + refresh(); + return; + } + + // Normal grid view attron(A_BOLD); - mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid"); + mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid (Grid %d)", selected_grid); attroff(A_BOLD); + // Draw grid selector bar at top + mvprintw(1, 0, "Grid: "); + for (int g = 0; g < NUM_GRIDS; g++) { + if (g == selected_grid) { + attron(A_REVERSE); + } + mvprintw(1, 6 + g * 4, "G%d", g); + if (g == selected_grid) { + attroff(A_REVERSE); + } + } + AppState state = dispatcher_get_state(); for (int row = 0; row < GRID_ROWS; row++) { for (int col = 0; col < GRID_COLS; col++) { bool selected = (row == selected_row && col == selected_col); - draw_cell(row, col, selected); + draw_cell(selected_grid, row, col, selected); } } - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); Clip *clip = &state.clips[clip_idx]; - mvprintw(GRID_ROWS * CELL_HEIGHT + 1, 0, - "Selected: Clip %d | State: %s | Buffer: %zu samples", - clip_idx, clip_state_to_string(clip->state), clip->buffer_size); + mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, + "Selected: Clip %d | Grid %d | State: %s | Buffer: %zu samples", + clip_idx, selected_grid, clip_state_to_string(clip->state), clip->buffer_size); - mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0, + mvprintw(GRID_ROWS * CELL_HEIGHT + 4, 0, "Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Threshold: %u", transport_state_to_string(state.transport_state), clock_source_to_string(state.clock_source), @@ -659,23 +722,24 @@ static void draw_grid(void) { case MODE_MOVE: mode_str = "MOVE"; break; default: mode_str = "?"; break; } - mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, "Mode: %s", mode_str); + mvprintw(GRID_ROWS * CELL_HEIGHT + 5, 0, "Mode: %s", mode_str); if (show_help) { attron(COLOR_PAIR(COLOR_HELP)); - mvprintw(GRID_ROWS * CELL_HEIGHT + 4, 0, "=== Help ==="); - mvprintw(GRID_ROWS * CELL_HEIGHT + 5, 0, "h/j/k/l - Navigate grid (left/down/up/right)"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 6, 0, "t - Trigger selected clip"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 7, 0, "r - Reset selected clip"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, "s - Trigger scene (current row)"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, "Space - Play/Pause transport"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "S - Stop transport"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "C - Toggle clock source (Internal/MIDI)"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "q - Toggle quantize mode (off/beat/bar)"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "T - Set quantize threshold"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0, "x - Reset transport position"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0, "? - Toggle help"); - mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0, "Esc/q - Quit"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 6, 0, "=== Help ==="); + mvprintw(GRID_ROWS * CELL_HEIGHT + 7, 0, "h/j/k/l - Navigate grid (left/down/up/right)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, "t - Trigger selected clip"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, "r - Reset selected clip"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "s - Trigger scene (current row)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "Space - Play/Pause transport"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "S - Stop transport"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "C - Toggle clock source (Internal/MIDI)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0, "q - Toggle quantize mode (off/beat/bar)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0, "T - Set quantize threshold"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0, "x - Reset transport position"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 17, 0, "z - Toggle grid selector (zoom mode)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 18, 0, "? - Toggle help"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 19, 0, "Esc/q - Quit"); attroff(COLOR_PAIR(COLOR_HELP)); } @@ -793,7 +857,7 @@ static bool handle_command_mode(void) { // Handle mouse events static void handle_mouse_event(MEVENT *event) { - int grid_row = (event->y - 1) / CELL_HEIGHT; + int grid_row = (event->y - 3) / CELL_HEIGHT; // Offset for grid selector int grid_col = (event->x - 1) / CELL_WIDTH; if (grid_row < 0 || grid_row >= GRID_ROWS || grid_col < 0 || grid_col >= GRID_COLS) { @@ -803,24 +867,24 @@ static void handle_mouse_event(MEVENT *event) { if (event->bstate & BUTTON1_CLICKED) { selected_row = grid_row; selected_col = grid_col; - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } }; g_dispatch(action); } else if (event->bstate & BUTTON3_CLICKED) { selected_row = grid_row; selected_col = grid_col; - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } }; g_dispatch(action); } else if (event->bstate & BUTTON2_CLICKED) { selected_row = grid_row; selected_col = grid_col; - Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row + selected_grid * GRID_ROWS } }; g_dispatch(action); } else if (event->bstate & BUTTON1_DOUBLE_CLICKED) { selected_row = grid_row; selected_col = grid_col; - Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row + selected_grid * GRID_ROWS } }; g_dispatch(action); } } @@ -916,6 +980,35 @@ void tui_run(Engine *engine) { } } + // Handle zoom mode navigation + if (zoom_mode) { + switch (ch) { + case 'h': case KEY_LEFT: + selected_grid = (selected_grid - 1 + NUM_GRIDS) % NUM_GRIDS; + break; + case 'l': case KEY_RIGHT: + selected_grid = (selected_grid + 1) % NUM_GRIDS; + break; + case 'j': case KEY_DOWN: + selected_grid = (selected_grid + 1) % NUM_GRIDS; + break; + case 'k': case KEY_UP: + selected_grid = (selected_grid - 1 + NUM_GRIDS) % NUM_GRIDS; + break; + case '\n': case '\r': + // Select this grid and exit zoom mode + zoom_mode = false; + break; + case 'z': + zoom_mode = false; + break; + case 27: case 'Q': + return; + } + draw_grid(); + continue; + } + if (current_mode == MODE_MOVE) { switch (ch) { case 'h': case KEY_LEFT: @@ -995,19 +1088,19 @@ void tui_run(Engine *engine) { selected_col = (selected_col + 1) % GRID_COLS; break; case 't': { - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } }; g_dispatch(action); break; } case 'd': { - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } }; g_dispatch(action); break; } case 'y': { - int clip_idx = grid_to_clip_index(selected_row, selected_col); + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); yank_clips(&clip_idx, 1); break; } @@ -1015,7 +1108,7 @@ void tui_run(Engine *engine) { paste_clips(); break; case 's': { - Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row + selected_grid * GRID_ROWS } }; g_dispatch(action); break; } @@ -1070,6 +1163,10 @@ void tui_run(Engine *engine) { if (should_quit) return; break; } + case 'z': { + zoom_mode = !zoom_mode; + break; + } case '?': show_help = !show_help; break; diff --git a/tui.h b/tui.h index 6c2b49d..eef59b3 100644 --- a/tui.h +++ b/tui.h @@ -4,6 +4,8 @@ #include "engine.h" #include "dispatcher.h" +#define NUM_GRIDS 8 + // Initialize TUI void tui_init(Engine *engine);