diff --git a/cli.c b/cli.c index 11d4bae..34207e1 100644 --- a/cli.c +++ b/cli.c @@ -58,6 +58,7 @@ int cli_process_line(Engine *engine, const char *line) { printf(" bpm - Set BPM (1.0-999.0)\n"); printf(" load - Load WAV file into clip\n"); printf(" save - Save clip to samples/clip_.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) { diff --git a/dispatcher.c b/dispatcher.c index ad830f6..69d76cf 100644 --- a/dispatcher.c +++ b/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; diff --git a/dispatcher.h b/dispatcher.h index 8611388..0d601e9 100644 --- a/dispatcher.h +++ b/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; diff --git a/engine.c b/engine.c index 8885480..13978ec 100644 --- a/engine.c +++ b/engine.c @@ -3,6 +3,7 @@ #include #include #include +#include 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); diff --git a/main.c b/main.c index 9eb6cf3..67155c2 100644 --- a/main.c +++ b/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); diff --git a/tui.c b/tui.c index 4df1b08..6f7dcd6 100644 --- a/tui.c +++ b/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");