feat: add parallel MIDI grid with separate clip storage and view toggle

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-03 18:49:21 +00:00
parent 5e4d4e4d44
commit 61ab2f0b19
6 changed files with 216 additions and 25 deletions

104
tui.c
View File

@@ -749,12 +749,16 @@ static bool handle_rack_view(int ch) {
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 + 3; // Offset by 2 for grid selector
int x = col * CELL_WIDTH + 1;
int color = state_to_color(clip->state);
int color;
if (state.show_midi_grid) {
color = state_to_color(state.midi_clips[clip_idx].state);
} else {
color = state_to_color(state.clips[clip_idx].state);
}
if (selected) {
color = COLOR_SELECTED;
} else if (current_mode == MODE_VISUAL && is_in_visual_selection(row, col)) {
@@ -769,17 +773,31 @@ static void draw_cell(int grid, int row, int col, bool selected) {
}
}
mvprintw(y + 1, x + 1, "%2d", clip_idx);
char state_char;
switch (clip->state) {
case CLIP_EMPTY: state_char = ' '; break;
case CLIP_RECORDING: state_char = 'R'; break;
case CLIP_LOOPING: state_char = 'L'; break;
case CLIP_STOPPED: state_char = 'S'; break;
default: state_char = '?'; break;
if (state.show_midi_grid) {
MidiClip *mclip = &state.midi_clips[clip_idx];
mvprintw(y + 1, x + 1, "%2d", clip_idx);
char state_char;
switch (mclip->state) {
case CLIP_EMPTY: state_char = ' '; break;
case CLIP_RECORDING: state_char = 'R'; break;
case CLIP_LOOPING: state_char = 'L'; break;
case CLIP_STOPPED: state_char = 'S'; break;
default: state_char = '?'; break;
}
mvaddch(y + 1, x + 4, state_char);
} else {
Clip *clip = &state.clips[clip_idx];
mvprintw(y + 1, x + 1, "%2d", clip_idx);
char state_char;
switch (clip->state) {
case CLIP_EMPTY: state_char = ' '; break;
case CLIP_RECORDING: state_char = 'R'; break;
case CLIP_LOOPING: state_char = 'L'; break;
case CLIP_STOPPED: state_char = 'S'; break;
default: state_char = '?'; break;
}
mvaddch(y + 1, x + 4, state_char);
}
mvaddch(y + 1, x + 4, state_char);
attroff(COLOR_PAIR(color));
}
@@ -827,7 +845,11 @@ static void draw_grid(void) {
// Normal grid view
attron(A_BOLD);
mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid (Grid %d)", selected_grid);
if (state.show_midi_grid) {
mvprintw(0, 0, "JACK Looper - MIDI Grid %d", selected_grid);
} else {
mvprintw(0, 0, "JACK Looper - Audio Grid %d", selected_grid);
}
attroff(A_BOLD);
// Draw grid selector bar at top
@@ -852,19 +874,26 @@ static void draw_grid(void) {
}
int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col);
Clip *clip = &state.clips[clip_idx];
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);
if (state.show_midi_grid) {
MidiClip *mclip = &state.midi_clips[clip_idx];
mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0,
"Selected: Clip %d | Grid %d | State: %s | MIDI events: %d",
clip_idx, selected_grid, clip_state_to_string(mclip->state), mclip->event_count);
} else {
Clip *clip = &state.clips[clip_idx];
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 + 4, 0,
"Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Threshold: %u",
"Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Grid: %s",
transport_state_to_string(state.transport_state),
clock_source_to_string(state.clock_source),
state.bpm,
quantize_mode_to_string(state.quantize_mode),
(unsigned int)state.quantize_threshold);
state.show_midi_grid ? "MIDI" : "AUDIO");
const char *mode_str;
switch (current_mode) {
@@ -893,6 +922,7 @@ static void draw_grid(void) {
mvprintw(GRID_ROWS * CELL_HEIGHT + 19, 0, "Esc/q - Quit");
mvprintw(GRID_ROWS * CELL_HEIGHT + 20, 0, "In fuzzy search: j/k navigate, h/l page, Enter select, Esc cancel");
mvprintw(GRID_ROWS * CELL_HEIGHT + 21, 0, "L - Load sample into selected clip (fuzzy search)");
mvprintw(GRID_ROWS * CELL_HEIGHT + 22, 0, "G - Toggle Audio/MIDI grid");
attroff(COLOR_PAIR(COLOR_HELP));
}
@@ -1217,7 +1247,15 @@ void tui_run(Engine *engine) {
int count;
int *clips = get_selected_clips(&count);
if (clips) {
delete_clips(clips, count);
AppState s = dispatcher_get_state();
if (s.show_midi_grid) {
for (int i = 0; i < count; i++) {
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = clips[i] } };
g_dispatch(action);
}
} else {
delete_clips(clips, count);
}
free(clips);
}
current_mode = MODE_NORMAL;
@@ -1257,14 +1295,26 @@ void tui_run(Engine *engine) {
break;
case 't': {
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);
AppState s = dispatcher_get_state();
if (s.show_midi_grid) {
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = clip_idx } };
g_dispatch(action);
} else {
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_grid, selected_row, selected_col);
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } };
g_dispatch(action);
AppState s = dispatcher_get_state();
if (s.show_midi_grid) {
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = clip_idx } };
g_dispatch(action);
} else {
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } };
g_dispatch(action);
}
break;
}
case 'y': {
@@ -1335,6 +1385,12 @@ void tui_run(Engine *engine) {
zoom_mode = !zoom_mode;
break;
}
case 'G': {
AppState s = dispatcher_get_state();
Action a = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = !s.show_midi_grid } };
g_dispatch(a);
break;
}
case 'L': {
// Start fuzzy search for WAV files
const char *samples_dir = getenv("JACK_LOOPER_SAMPLES_DIR");