diff --git a/Let me provide the SEARCH/REPLACE blocks.carla.h b/Let me provide the SEARCH/REPLACE blocks.carla.h new file mode 100644 index 0000000..c39cfcf --- /dev/null +++ b/Let me provide the SEARCH/REPLACE blocks.carla.h @@ -0,0 +1,83 @@ +#ifndef CARLA_H +#define CARLA_H + +#include +#include +#include + +// Carla plugin types we support +typedef enum { + PLUGIN_TYPE_NONE, + PLUGIN_TYPE_LV2, + PLUGIN_TYPE_VST2, + PLUGIN_TYPE_VST3, + PLUGIN_TYPE_CLAP, + PLUGIN_TYPE_INTERNAL // Our built-in plugins (gain, etc.) +} PluginType; + +// Plugin descriptor +typedef struct { + char name[256]; + char uri[512]; + PluginType type; + int carla_plugin_id; // Carla's internal plugin ID + int num_parameters; + float *parameters; // Current parameter values + char **parameter_names; +} PluginInfo; + +// Rack for a single channel +typedef struct { + int num_plugins; + PluginInfo plugins[16]; // Max 16 plugins per channel + float volume; // Channel volume (0.0 - 2.0) + bool bypassed; +} ChannelRack; + +// Carla host state +typedef struct { + bool initialized; + ChannelRack channel_racks[8]; // One rack per channel (MAX_CHANNELS) + jack_client_t *client; + // For plugin scanning + char **available_plugins; + int num_available_plugins; +} CarlaHost; + +// Initialize Carla host +int carla_init(CarlaHost *host, jack_client_t *client); + +// Cleanup Carla host +void carla_cleanup(CarlaHost *host); + +// Add a plugin to a channel's rack +int carla_add_plugin(CarlaHost *host, int channel, const char *uri, PluginType type); + +// Remove a plugin from a channel's rack +int carla_remove_plugin(CarlaHost *host, int channel, int plugin_index); + +// Set plugin parameter +int carla_set_parameter(CarlaHost *host, int channel, int plugin_index, int param_index, float value); + +// Get plugin parameter +float carla_get_parameter(CarlaHost *host, int channel, int plugin_index, int param_index); + +// Set channel volume +void carla_set_channel_volume(CarlaHost *host, int channel, float volume); + +// Get channel volume +float carla_get_channel_volume(CarlaHost *host, int channel); + +// Process audio through the rack (called from audio thread) +void carla_process(CarlaHost *host, int channel, float *in_buffer, float *out_buffer, jack_nframes_t nframes); + +// Scan for available plugins +int carla_scan_plugins(CarlaHost *host); + +// Get available plugin names for fuzzy search +const char** carla_get_available_plugins(CarlaHost *host, int *count); + +// Get plugin display name +const char* carla_get_plugin_name(CarlaHost *host, int channel, int plugin_index); + +#endif // CARLA_H diff --git a/carla.c b/carla.c index e69de29..796e7ee 100644 --- a/carla.c +++ b/carla.c @@ -0,0 +1,287 @@ +#include "carla.h" +#include +#include +#include +#include + +// Simple internal gain plugin +typedef struct { + float gain; +} GainPlugin; + +// Initialize Carla host +int carla_init(CarlaHost *host, jack_client_t *client) { + if (!host) return -1; + + memset(host, 0, sizeof(CarlaHost)); + host->client = client; + + // Initialize all channel racks + for (int ch = 0; ch < 8; ch++) { + host->channel_racks[ch].num_plugins = 0; + host->channel_racks[ch].volume = 1.0f; + host->channel_racks[ch].bypassed = false; + } + + // Initialize available plugins list with some defaults + host->num_available_plugins = 0; + host->available_plugins = NULL; + + // Try to initialize Carla native host + // In a real implementation, this would call carla_standalone_init() + // For now, we'll use our internal plugins + + host->initialized = true; + printf("Carla host initialized\n"); + + return 0; +} + +void carla_cleanup(CarlaHost *host) { + if (!host) return; + + // Clean up available plugins list + if (host->available_plugins) { + for (int i = 0; i < host->num_available_plugins; i++) { + free(host->available_plugins[i]); + } + free(host->available_plugins); + } + + // Clean up channel racks + for (int ch = 0; ch < 8; ch++) { + ChannelRack *rack = &host->channel_racks[ch]; + for (int p = 0; p < rack->num_plugins; p++) { + PluginInfo *plugin = &rack->plugins[p]; + if (plugin->parameters) { + free(plugin->parameters); + } + if (plugin->parameter_names) { + for (int i = 0; i < plugin->num_parameters; i++) { + free(plugin->parameter_names[i]); + } + free(plugin->parameter_names); + } + } + } + + host->initialized = false; +} + +// Add a built-in gain plugin +static int add_gain_plugin(CarlaHost *host, int channel) { + if (channel < 0 || channel >= 8) return -1; + ChannelRack *rack = &host->channel_racks[channel]; + if (rack->num_plugins >= 16) return -1; + + PluginInfo *plugin = &rack->plugins[rack->num_plugins]; + strcpy(plugin->name, "Gain"); + strcpy(plugin->uri, "internal://gain"); + plugin->type = PLUGIN_TYPE_INTERNAL; + plugin->carla_plugin_id = -1; + plugin->num_parameters = 1; + + plugin->parameters = malloc(sizeof(float)); + plugin->parameters[0] = 1.0f; // Default gain = 1.0 + + plugin->parameter_names = malloc(sizeof(char*)); + plugin->parameter_names[0] = strdup("Gain"); + + rack->num_plugins++; + return rack->num_plugins - 1; +} + +int carla_add_plugin(CarlaHost *host, int channel, const char *uri, PluginType type) { + if (!host || !host->initialized) return -1; + if (channel < 0 || channel >= 8) return -1; + + // For now, only support internal gain plugin + if (type == PLUGIN_TYPE_INTERNAL && strcmp(uri, "internal://gain") == 0) { + return add_gain_plugin(host, channel); + } + + // In a real implementation, this would call carla_add_plugin() from Carla API + // For now, fall back to gain plugin + return add_gain_plugin(host, channel); +} + +int carla_remove_plugin(CarlaHost *host, int channel, int plugin_index) { + if (!host || !host->initialized) return -1; + if (channel < 0 || channel >= 8) return -1; + + ChannelRack *rack = &host->channel_racks[channel]; + if (plugin_index < 0 || plugin_index >= rack->num_plugins) return -1; + + // Free plugin resources + PluginInfo *plugin = &rack->plugins[plugin_index]; + if (plugin->parameters) { + free(plugin->parameters); + } + if (plugin->parameter_names) { + for (int i = 0; i < plugin->num_parameters; i++) { + free(plugin->parameter_names[i]); + } + free(plugin->parameter_names); + } + + // Shift remaining plugins + for (int i = plugin_index; i < rack->num_plugins - 1; i++) { + rack->plugins[i] = rack->plugins[i + 1]; + } + + rack->num_plugins--; + return 0; +} + +int carla_set_parameter(CarlaHost *host, int channel, int plugin_index, int param_index, float value) { + if (!host || !host->initialized) return -1; + if (channel < 0 || channel >= 8) return -1; + + ChannelRack *rack = &host->channel_racks[channel]; + if (plugin_index < 0 || plugin_index >= rack->num_plugins) return -1; + + PluginInfo *plugin = &rack->plugins[plugin_index]; + if (param_index < 0 || param_index >= plugin->num_parameters) return -1; + + plugin->parameters[param_index] = value; + return 0; +} + +float carla_get_parameter(CarlaHost *host, int channel, int plugin_index, int param_index) { + if (!host || !host->initialized) return 0.0f; + if (channel < 0 || channel >= 8) return 0.0f; + + ChannelRack *rack = &host->channel_racks[channel]; + if (plugin_index < 0 || plugin_index >= rack->num_plugins) return 0.0f; + + PluginInfo *plugin = &rack->plugins[plugin_index]; + if (param_index < 0 || param_index >= plugin->num_parameters) return 0.0f; + + return plugin->parameters[param_index]; +} + +void carla_set_channel_volume(CarlaHost *host, int channel, float volume) { + if (!host || !host->initialized) return; + if (channel < 0 || channel >= 8) return; + + host->channel_racks[channel].volume = fmaxf(0.0f, fminf(2.0f, volume)); +} + +float carla_get_channel_volume(CarlaHost *host, int channel) { + if (!host || !host->initialized) return 1.0f; + if (channel < 0 || channel >= 8) return 1.0f; + + return host->channel_racks[channel].volume; +} + +void carla_process(CarlaHost *host, int channel, float *in_buffer, float *out_buffer, jack_nframes_t nframes) { + if (!host || !host->initialized) return; + if (channel < 0 || channel >= 8) return; + + ChannelRack *rack = &host->channel_racks[channel]; + + if (rack->bypassed || rack->num_plugins == 0) { + // Just apply volume + for (jack_nframes_t i = 0; i < nframes; i++) { + out_buffer[i] = in_buffer[i] * rack->volume; + } + return; + } + + // Process through plugin chain + float *current_in = in_buffer; + float *current_out = out_buffer; + + // Allocate temporary buffer for chaining + float *temp_buffer = malloc(nframes * sizeof(float)); + if (!temp_buffer) { + // Fallback to direct passthrough + for (jack_nframes_t i = 0; i < nframes; i++) { + out_buffer[i] = in_buffer[i] * rack->volume; + } + return; + } + + for (int p = 0; p < rack->num_plugins; p++) { + PluginInfo *plugin = &rack->plugins[p]; + + if (plugin->type == PLUGIN_TYPE_INTERNAL && strcmp(plugin->uri, "internal://gain") == 0) { + float gain = plugin->parameters[0]; + for (jack_nframes_t i = 0; i < nframes; i++) { + current_out[i] = current_in[i] * gain; + } + } + // In a real implementation, this would call Carla's process function + + // Chain: output of this plugin becomes input for next + if (p < rack->num_plugins - 1) { + float *swap = current_in; + current_in = current_out; + current_out = swap; + } + } + + // Apply channel volume + for (jack_nframes_t i = 0; i < nframes; i++) { + current_out[i] *= rack->volume; + } + + // If we ended up in temp_buffer, copy to out_buffer + if (current_out == temp_buffer) { + memcpy(out_buffer, temp_buffer, nframes * sizeof(float)); + } + + free(temp_buffer); +} + +int carla_scan_plugins(CarlaHost *host) { + if (!host || !host->initialized) return -1; + + // In a real implementation, this would scan for available LV2/VST plugins + // For now, add some built-in options + + // Clean up previous scan + if (host->available_plugins) { + for (int i = 0; i < host->num_available_plugins; i++) { + free(host->available_plugins[i]); + } + free(host->available_plugins); + } + + // Add built-in plugins + const char *builtins[] = { + "internal://gain - Gain (Built-in)", + "internal://reverb - Reverb (Built-in)", + "internal://delay - Delay (Built-in)", + "internal://filter - Filter (Built-in)" + }; + + host->num_available_plugins = 4; + host->available_plugins = malloc(host->num_available_plugins * sizeof(char*)); + + for (int i = 0; i < host->num_available_plugins; i++) { + host->available_plugins[i] = strdup(builtins[i]); + } + + return host->num_available_plugins; +} + +const char** carla_get_available_plugins(CarlaHost *host, int *count) { + if (!host || !host->initialized) { + if (count) *count = 0; + return NULL; + } + + if (count) *count = host->num_available_plugins; + return (const char**)host->available_plugins; +} + +const char* carla_get_plugin_name(CarlaHost *host, int channel, int plugin_index) { + if (!host || !host->initialized) return NULL; + if (channel < 0 || channel >= 8) return NULL; + + ChannelRack *rack = &host->channel_racks[channel]; + if (plugin_index < 0 || plugin_index >= rack->num_plugins) return NULL; + + return rack->plugins[plugin_index].name; +} diff --git a/dispatcher.c b/dispatcher.c index 3f737f4..af48359 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -368,6 +368,54 @@ AppState reducer(AppState state, Action action) { return scene_trigger(state, action.data.midi_scene_launch.scene_index); } + case ACTION_RACK_ADD_PLUGIN: { + int channel = action.data.rack_add_plugin.channel; + if (channel >= 0 && channel < MAX_CHANNELS) { + carla_add_plugin(&state.carla_host, channel, + action.data.rack_add_plugin.uri, + action.data.rack_add_plugin.type); + } + return state; + } + + case ACTION_RACK_REMOVE_PLUGIN: { + int channel = action.data.rack_remove_plugin.channel; + int plugin_idx = action.data.rack_remove_plugin.plugin_index; + if (channel >= 0 && channel < MAX_CHANNELS) { + carla_remove_plugin(&state.carla_host, channel, plugin_idx); + } + return state; + } + + case ACTION_RACK_SET_PARAMETER: { + int channel = action.data.rack_set_parameter.channel; + int plugin_idx = action.data.rack_set_parameter.plugin_index; + int param_idx = action.data.rack_set_parameter.param_index; + float value = action.data.rack_set_parameter.value; + if (channel >= 0 && channel < MAX_CHANNELS) { + carla_set_parameter(&state.carla_host, channel, plugin_idx, param_idx, value); + } + return state; + } + + case ACTION_RACK_SET_VOLUME: { + int channel = action.data.rack_set_volume.channel; + float volume = action.data.rack_set_volume.volume; + if (channel >= 0 && channel < MAX_CHANNELS) { + carla_set_channel_volume(&state.carla_host, channel, volume); + } + return state; + } + + case ACTION_RACK_BYPASS: { + int channel = action.data.rack_bypass.channel; + bool bypass = action.data.rack_bypass.bypass; + if (channel >= 0 && channel < MAX_CHANNELS) { + state.carla_host.channel_racks[channel].bypassed = bypass; + } + return state; + } + case ACTION_PROCESS_AUDIO: return state; diff --git a/dispatcher.h b/dispatcher.h index 2e0c681..80e9875 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -6,6 +6,7 @@ #include #include #include +#include "carla.h" // ============================================================ // Application State - contains ALL application state @@ -82,6 +83,9 @@ typedef struct { size_t prev_read_positions[MAX_UNDO_HISTORY]; } undo; + // Carla host + CarlaHost carla_host; + // JACK info (needed by audio thread) jack_nframes_t sample_rate; bool running; @@ -110,6 +114,11 @@ typedef enum { ACTION_LOAD_CLIP, ACTION_MIDI_NOTE_ON, ACTION_MIDI_SCENE_LAUNCH, + ACTION_RACK_ADD_PLUGIN, + ACTION_RACK_REMOVE_PLUGIN, + ACTION_RACK_SET_PARAMETER, + ACTION_RACK_SET_VOLUME, + ACTION_RACK_BYPASS, ACTION_PROCESS_AUDIO, ACTION_QUIT } ActionType; @@ -128,6 +137,11 @@ typedef struct { struct { int clip_index; char filename[256]; } load_clip; struct { int note; int velocity; int channel; jack_nframes_t time; } midi_note_on; struct { int scene_index; jack_nframes_t time; } midi_scene_launch; + struct { int channel; char uri[512]; PluginType type; } rack_add_plugin; + struct { int channel; int plugin_index; } rack_remove_plugin; + struct { int channel; int plugin_index; int param_index; float value; } rack_set_parameter; + struct { int channel; float volume; } rack_set_volume; + struct { int channel; bool bypass; } rack_bypass; struct { jack_nframes_t nframes; } process_audio; } data; } Action; diff --git a/engine.c b/engine.c index 884be43..8a7bd5c 100644 --- a/engine.c +++ b/engine.c @@ -80,7 +80,19 @@ static int process_callback(jack_nframes_t nframes, void *arg) { for (int ch = 0; ch < MAX_CHANNELS; ch++) { memset(audio_out[ch], 0, sizeof(jack_default_audio_sample_t) * nframes); + // Create temporary buffer for rack processing + float *rack_in = malloc(nframes * sizeof(float)); + float *rack_out = malloc(nframes * sizeof(float)); + + if (!rack_in || !rack_out) { + free(rack_in); + free(rack_out); + continue; + } + for (jack_nframes_t i = 0; i < nframes; i++) { + 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]; @@ -92,11 +104,20 @@ static int process_callback(jack_nframes_t nframes, void *arg) { } if (clip->state == CLIP_LOOPING && clip->buffer_size > 0) { - audio_out[ch][i] += clip->buffer[clip->read_position]; + rack_in[i] += clip->buffer[clip->read_position]; clip->read_position = (clip->read_position + 1) % clip->buffer_size; } } } + + // Process through Carla rack + carla_process(&state.carla_host, ch, rack_in, rack_out, nframes); + + // Copy to output + memcpy(audio_out[ch], rack_out, nframes * sizeof(jack_default_audio_sample_t)); + + free(rack_in); + free(rack_out); } return 0; diff --git a/makefile b/makefile index df33714..068d557 100644 --- a/makefile +++ b/makefile @@ -4,7 +4,7 @@ LDFLAGS = -ljack -lm -lncurses -lpthread all: jack-looper test_engine test_tui test_gui test_cli test_stress test_wav_io -jack-looper: main.o engine.o tui.o gui.o cli.o transport.o dispatcher.o lib/microui.o wav_io.o +jack-looper: main.o engine.o tui.o gui.o cli.o transport.o dispatcher.o lib/microui.o wav_io.o carla.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_engine: test_engine.o engine.o transport.o wav_io.o dispatcher.o @@ -34,13 +34,13 @@ test_stress.o: test_stress.c dispatcher.h engine.h wav_io.h main.o: main.c engine.h tui.h transport.h $(CC) $(CFLAGS) -c -o $@ $< -engine.o: engine.c engine.h transport.h wav_io.h +engine.o: engine.c engine.h transport.h wav_io.h carla.h $(CC) $(CFLAGS) -c -o $@ $< wav_io.o: wav_io.c wav_io.h $(CC) $(CFLAGS) -c -o $@ $< -tui.o: tui.c tui.h engine.h transport.h +tui.o: tui.c tui.h engine.h transport.h carla.h $(CC) $(CFLAGS) -c -o $@ $< gui.o: gui.c gui.h engine.h transport.h lib/microui.h @@ -52,7 +52,10 @@ lib/microui.o: lib/microui.c lib/microui.h transport.o: transport.c transport.h $(CC) $(CFLAGS) -c -o $@ $< -dispatcher.o: dispatcher.c dispatcher.h +carla.o: carla.c carla.h + $(CC) $(CFLAGS) -c -o $@ $< + +dispatcher.o: dispatcher.c dispatcher.h carla.h $(CC) $(CFLAGS) -c -o $@ $< test_engine.o: test_engine.c engine.h transport.h diff --git a/tui.c b/tui.c index 565f78c..460c02e 100644 --- a/tui.c +++ b/tui.c @@ -1,11 +1,13 @@ #include "tui.h" #include "wav_io.h" #include "transport.h" +#include "carla.h" #include #include #include #include #include +#include #define GRID_ROWS 8 #define GRID_COLS 8 @@ -28,6 +30,31 @@ static int selected_row = 0; static int selected_col = 0; static bool show_help = false; +// View modes +typedef enum { + VIEW_GRID, + VIEW_RACK +} UIView; + +static UIView current_view = VIEW_GRID; +static int rack_selected_channel = 0; +static int rack_selected_plugin = -1; +static int rack_selected_param = -1; + +// Fuzzy search state +typedef struct { + char query[256]; + int query_len; + int selected_index; + int num_results; + int result_indices[256]; + bool active; + char prompt[64]; + void (*callback)(const char *selection); +} FuzzySearch; + +static FuzzySearch fuzzy_search = {0}; + // Modes typedef enum { MODE_NORMAL, @@ -194,6 +221,345 @@ static void play_prev_scene(void) { selected_row = prev_row; } +// Fuzzy matching function +static bool fuzzy_match(const char *pattern, const char *text) { + if (!pattern || !*pattern) return true; + if (!text) return false; + + while (*pattern) { + char p = *pattern; + while (*text && tolower((unsigned char)*text) != tolower((unsigned char)p)) text++; + if (!*text) return false; + text++; + pattern++; + } + return true; +} + +// Draw fuzzy search +static void draw_fuzzy_search(void) { + int start_y = LINES / 3; + int start_x = COLS / 4; + int width = COLS / 2; + int height = LINES / 3; + + // Draw border + for (int y = start_y - 1; y <= start_y + height; y++) { + for (int x = start_x - 1; x <= start_x + width; x++) { + mvaddch(y, x, ' '); + } + } + + // Draw title + attron(A_BOLD); + mvprintw(start_y - 1, start_x, "%s", fuzzy_search.prompt); + attroff(A_BOLD); + + // Draw search box + mvprintw(start_y, start_x, "> %s", fuzzy_search.query); + + // Draw results + for (int i = 0; i < height - 2 && i < fuzzy_search.num_results; i++) { + int idx = fuzzy_search.result_indices[i]; + if (i == fuzzy_search.selected_index) { + attron(A_REVERSE); + } + int count; + const char **plugins = carla_get_available_plugins(NULL, &count); + if (plugins && idx >= 0 && idx < count) { + mvprintw(start_y + 1 + i, start_x, "%s", plugins[idx]); + } + if (i == fuzzy_search.selected_index) { + attroff(A_REVERSE); + } + } +} + +// Handle fuzzy search input +static bool handle_fuzzy_search(int ch) { + if (!fuzzy_search.active) return false; + + switch (ch) { + case '\n': case '\r': { + if (fuzzy_search.num_results > 0 && fuzzy_search.selected_index >= 0) { + int idx = fuzzy_search.result_indices[fuzzy_search.selected_index]; + int count; + const char **plugins = carla_get_available_plugins(NULL, &count); + if (plugins && idx >= 0 && idx < count && fuzzy_search.callback) { + fuzzy_search.callback(plugins[idx]); + } + } + fuzzy_search.active = false; + return true; + } + case 27: { // ESC + fuzzy_search.active = false; + return true; + } + case KEY_BACKSPACE: case 127: { + if (fuzzy_search.query_len > 0) { + fuzzy_search.query[--fuzzy_search.query_len] = '\0'; + // Update results + fuzzy_search.num_results = 0; + int count; + const char **plugins = carla_get_available_plugins(NULL, &count); + for (int i = 0; i < count; i++) { + if (fuzzy_match(fuzzy_search.query, plugins[i])) { + fuzzy_search.result_indices[fuzzy_search.num_results++] = i; + } + } + if (fuzzy_search.selected_index >= fuzzy_search.num_results) { + fuzzy_search.selected_index = fuzzy_search.num_results - 1; + } + } + return true; + } + case KEY_UP: { + if (fuzzy_search.selected_index > 0) { + fuzzy_search.selected_index--; + } + return true; + } + case KEY_DOWN: { + if (fuzzy_search.selected_index < fuzzy_search.num_results - 1) { + fuzzy_search.selected_index++; + } + return true; + } + default: { + if (ch >= 32 && ch <= 126 && fuzzy_search.query_len < 255) { + fuzzy_search.query[fuzzy_search.query_len++] = (char)ch; + fuzzy_search.query[fuzzy_search.query_len] = '\0'; + // Update results + fuzzy_search.num_results = 0; + int count; + const char **plugins = carla_get_available_plugins(NULL, &count); + for (int i = 0; i < count; i++) { + if (fuzzy_match(fuzzy_search.query, plugins[i])) { + fuzzy_search.result_indices[fuzzy_search.num_results++] = i; + } + } + fuzzy_search.selected_index = 0; + } + return true; + } + } +} + +// Start fuzzy search +static void start_fuzzy_search(const char *prompt, void (*callback)(const char *)) { + fuzzy_search.active = true; + fuzzy_search.query_len = 0; + fuzzy_search.query[0] = '\0'; + fuzzy_search.selected_index = 0; + strncpy(fuzzy_search.prompt, prompt, sizeof(fuzzy_search.prompt) - 1); + fuzzy_search.prompt[sizeof(fuzzy_search.prompt) - 1] = '\0'; + fuzzy_search.callback = callback; + + // Initialize results with all plugins + fuzzy_search.num_results = 0; + int count; + const char **plugins = carla_get_available_plugins(NULL, &count); + for (int i = 0; i < count; i++) { + fuzzy_search.result_indices[fuzzy_search.num_results++] = i; + } +} + +// Draw rack view +static void draw_rack_view(void) { + clear(); + + attron(A_BOLD); + mvprintw(0, 0, "JACK Looper - Rack View (Channel %d)", rack_selected_channel); + attroff(A_BOLD); + + AppState state = dispatcher_get_state(); + ChannelRack *rack = &state.carla_host.channel_racks[rack_selected_channel]; + + // Draw channel selector + mvprintw(1, 0, "Channel: "); + for (int ch = 0; ch < 8; ch++) { + if (ch == rack_selected_channel) { + attron(A_REVERSE); + } + mvprintw(1, 10 + ch * 4, "Ch%d", ch); + if (ch == rack_selected_channel) { + attroff(A_REVERSE); + } + } + + // Draw volume + mvprintw(2, 0, "Volume: %.2f [ - ] to decrease, [ = ] to increase", rack->volume); + + // Draw bypass toggle + mvprintw(3, 0, "Bypass: %s [b] to toggle", rack->bypassed ? "ON" : "OFF"); + + // Draw plugin chain + mvprintw(4, 0, "=== Plugin Chain ==="); + + int y = 5; + for (int p = 0; p < rack->num_plugins; p++) { + PluginInfo *plugin = &rack->plugins[p]; + + if (p == rack_selected_plugin) { + attron(A_REVERSE); + } + mvprintw(y++, 2, "[%d] %s", p, plugin->name); + if (p == rack_selected_plugin) { + attroff(A_REVERSE); + } + + // Draw parameters if this plugin is selected + if (p == rack_selected_plugin) { + for (int param = 0; param < plugin->num_parameters; param++) { + if (param == rack_selected_param) { + attron(A_REVERSE); + } + mvprintw(y++, 4, "%s: %.3f", + plugin->parameter_names[param], + plugin->parameters[param]); + if (param == rack_selected_param) { + attroff(A_REVERSE); + } + } + } + } + + // Draw controls help + mvprintw(y + 1, 0, "=== Controls ==="); + mvprintw(y + 2, 0, "h/l - Previous/Next channel"); + mvprintw(y + 3, 0, "j/k - Navigate plugins"); + mvprintw(y + 4, 0, "a - Add plugin to rack"); + mvprintw(y + 5, 0, "d - Remove selected plugin"); + mvprintw(y + 6, 0, "b - Toggle bypass"); + mvprintw(y + 7, 0, "-/= - Decrease/Increase volume"); + mvprintw(y + 8, 0, "Enter - Select parameter to edit"); + mvprintw(y + 9, 0, "Tab - Switch to grid view"); + mvprintw(y + 10, 0, "q/Esc - Quit"); + + refresh(); +} + +// Handle rack view input +static bool handle_rack_view(int ch) { + AppState state = dispatcher_get_state(); + ChannelRack *rack = &state.carla_host.channel_racks[rack_selected_channel]; + + switch (ch) { + case 'h': case KEY_LEFT: + rack_selected_channel = (rack_selected_channel - 1 + 8) % 8; + rack_selected_plugin = -1; + rack_selected_param = -1; + break; + case 'l': case KEY_RIGHT: + rack_selected_channel = (rack_selected_channel + 1) % 8; + rack_selected_plugin = -1; + rack_selected_param = -1; + break; + case 'j': case KEY_DOWN: + if (rack_selected_plugin < rack->num_plugins - 1) { + rack_selected_plugin++; + rack_selected_param = -1; + } + break; + case 'k': case KEY_UP: + if (rack_selected_plugin > 0) { + rack_selected_plugin--; + rack_selected_param = -1; + } else if (rack_selected_plugin == 0) { + rack_selected_plugin = -1; + } + break; + case 'a': { + // Start fuzzy search for plugin selection + start_fuzzy_search("Add plugin:", [](const char *selection) { + if (selection) { + // Parse URI from selection (format: "uri - name") + char uri[512]; + const char *space = strchr(selection, ' '); + if (space) { + size_t len = space - selection; + strncpy(uri, selection, len); + uri[len] = '\0'; + } else { + strncpy(uri, selection, sizeof(uri) - 1); + } + + Action action = { + .type = ACTION_RACK_ADD_PLUGIN, + .data.rack_add_plugin = { + .channel = rack_selected_channel, + .type = PLUGIN_TYPE_INTERNAL + } + }; + strncpy(action.data.rack_add_plugin.uri, uri, sizeof(action.data.rack_add_plugin.uri) - 1); + action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0'; + g_dispatch(action); + } + }); + break; + } + case 'd': { + if (rack_selected_plugin >= 0 && rack_selected_plugin < rack->num_plugins) { + Action action = { + .type = ACTION_RACK_REMOVE_PLUGIN, + .data.rack_remove_plugin = { + .channel = rack_selected_channel, + .plugin_index = rack_selected_plugin + } + }; + g_dispatch(action); + rack_selected_plugin = -1; + rack_selected_param = -1; + } + break; + } + case 'b': { + Action action = { + .type = ACTION_RACK_BYPASS, + .data.rack_bypass = { + .channel = rack_selected_channel, + .bypass = !rack->bypassed + } + }; + g_dispatch(action); + break; + } + case '-': case '_': { + float new_vol = fmaxf(0.0f, rack->volume - 0.1f); + Action action = { + .type = ACTION_RACK_SET_VOLUME, + .data.rack_set_volume = { + .channel = rack_selected_channel, + .volume = new_vol + } + }; + g_dispatch(action); + break; + } + case '=': case '+': { + float new_vol = fminf(2.0f, rack->volume + 0.1f); + Action action = { + .type = ACTION_RACK_SET_VOLUME, + .data.rack_set_volume = { + .channel = rack_selected_channel, + .volume = new_vol + } + }; + g_dispatch(action); + break; + } + case '\t': { + current_view = VIEW_GRID; + break; + } + case 'q': case 27: + return true; // Quit + } + + return false; +} + // Draw a single cell static void draw_cell(int row, int col, bool selected) { int clip_idx = grid_to_clip_index(row, col); @@ -318,6 +684,40 @@ static bool handle_command_mode(void) { if (strcmp(cmd_buffer, "q") == 0) { nodelay(stdscr, prev_nodelay); return true; + } else if (strcmp(cmd_buffer, "view rack") == 0) { + current_view = VIEW_RACK; + rack_selected_channel = selected_row; + rack_selected_plugin = -1; + rack_selected_param = -1; + } else if (strncmp(cmd_buffer, "rack ", 5) == 0) { + // :rack - add plugin to selected channel's rack + const char *plugin_name = cmd_buffer + 5; + Action action = { + .type = ACTION_RACK_ADD_PLUGIN, + .data.rack_add_plugin = { + .channel = selected_row, + .type = PLUGIN_TYPE_INTERNAL + } + }; + strncpy(action.data.rack_add_plugin.uri, plugin_name, sizeof(action.data.rack_add_plugin.uri) - 1); + action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0'; + g_dispatch(action); + } else if (strcmp(cmd_buffer, "from") == 0) { + // :from - open fuzzy search for MIDI input source + start_fuzzy_search("Select MIDI input source:", [](const char *selection) { + if (selection) { + printf("Selected MIDI input: %s\n", selection); + // In a real implementation, this would connect JACK ports + } + }); + } else if (strcmp(cmd_buffer, "to") == 0) { + // :to - open fuzzy search for MIDI output destination + start_fuzzy_search("Select MIDI output destination:", [](const char *selection) { + if (selection) { + printf("Selected MIDI output: %s\n", selection); + // In a real implementation, this would connect JACK ports + } + }); } else if (strncmp(cmd_buffer, "load ", 5) == 0) { char *rest = cmd_buffer + 5; int clip_idx = atoi(rest); @@ -434,9 +834,42 @@ void tui_run(Engine *engine) { marks[i] = -1; } + // Initialize Carla + AppState init_state = dispatcher_get_state(); + carla_init(&init_state.carla_host, engine->client); + carla_scan_plugins(&init_state.carla_host); + draw_grid(); while (1) { + // Handle fuzzy search first + if (fuzzy_search.active) { + draw_fuzzy_search(); + int ch = getch(); + if (handle_fuzzy_search(ch)) { + // Fuzzy search closed, redraw current view + if (current_view == VIEW_GRID) { + draw_grid(); + } else { + draw_rack_view(); + } + } else { + draw_fuzzy_search(); + } + continue; + } + + // Handle current view + if (current_view == VIEW_RACK) { + draw_rack_view(); + int ch = getch(); + if (handle_rack_view(ch)) { + return; // Quit + } + continue; + } + + // Grid view (existing code) int ch = getch(); if (ch == ERR) { break; @@ -651,6 +1084,44 @@ void tui_run(Engine *engine) { g_dispatch(action); break; } + case '-': case '_': { + // Decrease volume of selected channel + AppState state = dispatcher_get_state(); + float current_vol = carla_get_channel_volume(&state.carla_host, selected_row); + float new_vol = fmaxf(0.0f, current_vol - 0.1f); + Action action = { + .type = ACTION_RACK_SET_VOLUME, + .data.rack_set_volume = { + .channel = selected_row, + .volume = new_vol + } + }; + g_dispatch(action); + break; + } + case '=': case '+': { + // Increase volume of selected channel + AppState state = dispatcher_get_state(); + float current_vol = carla_get_channel_volume(&state.carla_host, selected_row); + float new_vol = fminf(2.0f, current_vol + 0.1f); + Action action = { + .type = ACTION_RACK_SET_VOLUME, + .data.rack_set_volume = { + .channel = selected_row, + .volume = new_vol + } + }; + g_dispatch(action); + break; + } + case '\t': { + // Switch to rack view + current_view = VIEW_RACK; + rack_selected_channel = selected_row; + rack_selected_plugin = -1; + rack_selected_param = -1; + break; + } case 27: case 'Q': return; default: