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:
12
cli.c
12
cli.c
@@ -58,6 +58,7 @@ int cli_process_line(Engine *engine, const char *line) {
|
||||
printf(" bpm <value> - Set BPM (1.0-999.0)\n");
|
||||
printf(" load <clip> <file> - Load WAV file into clip\n");
|
||||
printf(" save <clip> - Save clip to samples/clip_<N>.wav\n");
|
||||
printf(" grid audio|midi - Switch between Audio and MIDI grid\n");
|
||||
printf(" help - Show this help\n");
|
||||
printf(" quit - Exit CLI\n");
|
||||
return 1;
|
||||
@@ -189,6 +190,17 @@ int cli_process_line(Engine *engine, const char *line) {
|
||||
engine->dispatch(action);
|
||||
printf("Saving clip %d...\n", clip_idx);
|
||||
}
|
||||
else if (strcasecmp(token, "grid") == 0) {
|
||||
char *mode_str = strtok(NULL, " \t");
|
||||
if (!mode_str) {
|
||||
printf("Usage: grid audio|midi\n");
|
||||
return 1;
|
||||
}
|
||||
bool show_midi = (strcasecmp(mode_str, "midi") == 0);
|
||||
Action action = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = show_midi } };
|
||||
engine->dispatch(action);
|
||||
printf("Grid: %s\n", show_midi ? "MIDI" : "AUDIO");
|
||||
}
|
||||
else if (strcasecmp(token, "bpm") == 0) {
|
||||
char *bpm_str = strtok(NULL, " \t");
|
||||
if (!bpm_str) {
|
||||
|
||||
54
dispatcher.c
54
dispatcher.c
@@ -167,6 +167,34 @@ static AppState clip_trigger(AppState state, int clip_index) {
|
||||
return state;
|
||||
}
|
||||
|
||||
static AppState midi_clip_trigger(AppState state, int clip_index) {
|
||||
if (clip_index < 0 || clip_index >= MAX_CLIPS) return state;
|
||||
|
||||
MidiClip *clip = &state.midi_clips[clip_index];
|
||||
|
||||
switch (clip->state) {
|
||||
case CLIP_EMPTY:
|
||||
clip->state = CLIP_RECORDING;
|
||||
clip->event_count = 0;
|
||||
clip->read_index = 0;
|
||||
break;
|
||||
case CLIP_RECORDING:
|
||||
clip->state = CLIP_LOOPING;
|
||||
clip->read_index = 0;
|
||||
break;
|
||||
case CLIP_LOOPING:
|
||||
clip->state = CLIP_STOPPED;
|
||||
clip->read_index = 0;
|
||||
break;
|
||||
case CLIP_STOPPED:
|
||||
clip->state = CLIP_LOOPING;
|
||||
clip->read_index = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
static AppState scene_trigger(AppState state, int scene_index) {
|
||||
if (scene_index < 0 || scene_index >= MAX_SCENES * 8) return state; // 8 grids
|
||||
|
||||
@@ -454,6 +482,32 @@ AppState reducer(AppState state, Action action) {
|
||||
case ACTION_PROCESS_AUDIO:
|
||||
return state;
|
||||
|
||||
case ACTION_SET_SHOW_MIDI_GRID:
|
||||
state.show_midi_grid = action.data.set_show_midi_grid.show;
|
||||
return state;
|
||||
|
||||
case ACTION_MIDI_CLIP_TRIGGER:
|
||||
return midi_clip_trigger(state, action.data.midi_clip_trigger.clip_index);
|
||||
|
||||
case ACTION_MIDI_CLIP_RESET: {
|
||||
int idx = action.data.midi_clip_reset.clip_index;
|
||||
if (idx >= 0 && idx < MAX_CLIPS) {
|
||||
state.midi_clips[idx].state = CLIP_EMPTY;
|
||||
state.midi_clips[idx].event_count = 0;
|
||||
state.midi_clips[idx].read_index = 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ACTION_SET_CHANNEL_NAME: {
|
||||
int ch = action.data.set_channel_name.channel;
|
||||
if (ch >= 0 && ch < MAX_CHANNELS) {
|
||||
strncpy(state.channel_names[ch], action.data.set_channel_name.name, 63);
|
||||
state.channel_names[ch][63] = '\0';
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ACTION_SAVE_PROJECT: {
|
||||
fs_save_project(action.data.save_project.filename, &state);
|
||||
return state;
|
||||
|
||||
31
dispatcher.h
31
dispatcher.h
@@ -46,6 +46,22 @@ typedef enum {
|
||||
CLOCK_SOURCE_MIDI
|
||||
} ClockSource;
|
||||
|
||||
typedef struct {
|
||||
uint8_t note;
|
||||
uint8_t velocity;
|
||||
uint32_t timestamp; // sample offset within the clip
|
||||
} MidiEvent;
|
||||
|
||||
#define MAX_MIDI_EVENTS 44100 // ~1 second of dense MIDI data
|
||||
|
||||
typedef struct {
|
||||
ClipState state;
|
||||
MidiEvent events[MAX_MIDI_EVENTS];
|
||||
int event_count;
|
||||
int read_index;
|
||||
char channel_name[64]; // per-channel name for MIDI grid
|
||||
} MidiClip;
|
||||
|
||||
typedef struct {
|
||||
ClipState state;
|
||||
float *buffer;
|
||||
@@ -73,6 +89,11 @@ typedef struct {
|
||||
// Clips
|
||||
Clip clips[MAX_CLIPS];
|
||||
|
||||
// MIDI clips (separate grid)
|
||||
MidiClip midi_clips[MAX_CLIPS];
|
||||
bool show_midi_grid; // View toggle: true = MIDI grid, false = Audio grid
|
||||
char channel_names[MAX_CHANNELS][64]; // Channel names for audio grid
|
||||
|
||||
// Undo history
|
||||
struct {
|
||||
int undo_index;
|
||||
@@ -126,7 +147,11 @@ typedef enum {
|
||||
ACTION_SAVE_PROJECT,
|
||||
ACTION_LOAD_PROJECT,
|
||||
ACTION_PROCESS_AUDIO,
|
||||
ACTION_QUIT
|
||||
ACTION_QUIT,
|
||||
ACTION_SET_SHOW_MIDI_GRID,
|
||||
ACTION_MIDI_CLIP_TRIGGER,
|
||||
ACTION_MIDI_CLIP_RESET,
|
||||
ACTION_SET_CHANNEL_NAME
|
||||
} ActionType;
|
||||
|
||||
typedef struct {
|
||||
@@ -151,6 +176,10 @@ typedef struct {
|
||||
struct { char filename[512]; } save_project;
|
||||
struct { char filename[512]; } load_project;
|
||||
struct { jack_nframes_t nframes; } process_audio;
|
||||
struct { bool show; } set_show_midi_grid;
|
||||
struct { int clip_index; } midi_clip_trigger;
|
||||
struct { int clip_index; } midi_clip_reset;
|
||||
struct { int channel; char name[64]; } set_channel_name;
|
||||
} data;
|
||||
} Action;
|
||||
|
||||
|
||||
29
engine.c
29
engine.c
@@ -3,6 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
static int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
Engine *engine = (Engine *)arg;
|
||||
@@ -77,6 +78,34 @@ static int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
// MIDI clip playback
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
for (int s = 0; s < MAX_SCENES; s++) {
|
||||
for (int g = 0; g < 8; g++) {
|
||||
int clip_idx = g * GRID_ROWS * GRID_COLS + s * GRID_COLS + ch;
|
||||
MidiClip *mclip = &state.midi_clips[clip_idx];
|
||||
|
||||
if (mclip->state == CLIP_LOOPING && mclip->event_count > 0) {
|
||||
if (mclip->read_index < mclip->event_count) {
|
||||
int idx = mclip->read_index;
|
||||
uint8_t msg[3] = {
|
||||
0x90 | ch,
|
||||
mclip->events[idx].note,
|
||||
mclip->events[idx].velocity
|
||||
};
|
||||
jack_midi_event_write(midi_out_buf,
|
||||
mclip->events[idx].timestamp, msg, 3);
|
||||
mclip->read_index++;
|
||||
|
||||
if (mclip->read_index >= mclip->event_count) {
|
||||
mclip->read_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process audio per-channel
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
memset(audio_out[ch], 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||
|
||||
11
main.c
11
main.c
@@ -95,8 +95,19 @@ int main(int argc, char *argv[]) {
|
||||
initial_state.clips[i].buffer_size = 0;
|
||||
initial_state.clips[i].write_position = 0;
|
||||
initial_state.clips[i].read_position = 0;
|
||||
|
||||
// Initialize MIDI clips
|
||||
initial_state.midi_clips[i].state = CLIP_EMPTY;
|
||||
initial_state.midi_clips[i].event_count = 0;
|
||||
initial_state.midi_clips[i].read_index = 0;
|
||||
}
|
||||
|
||||
// Initialize channel names
|
||||
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
|
||||
snprintf(initial_state.channel_names[ch], 64, "Channel %d", ch);
|
||||
}
|
||||
initial_state.show_midi_grid = false;
|
||||
|
||||
// Carla host is now initialized in engine_init
|
||||
// Initialize dispatcher
|
||||
dispatch = dispatcher_init(&initial_state);
|
||||
|
||||
104
tui.c
104
tui.c
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user