381 lines
13 KiB
C
381 lines
13 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <jack/jack.h>
|
|
#include <ncurses.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
#include "engine.h"
|
|
#include "gui.h"
|
|
#include "dispatcher.h"
|
|
#include "carla.h"
|
|
|
|
/* microui includes */
|
|
#define MU_IMPLEMENTATION
|
|
#include "microui.h"
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* microui callbacks
|
|
* ------------------------------------------------------------------------- */
|
|
static int text_width(mu_Font font, const char *text, int len)
|
|
{
|
|
(void)font;
|
|
if (len == -1) { len = strlen(text); }
|
|
return len * 8;
|
|
}
|
|
|
|
static int text_height(mu_Font font)
|
|
{
|
|
(void)font;
|
|
return 18;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* global state
|
|
* ------------------------------------------------------------------------- */
|
|
static mu_Context *ctx = NULL;
|
|
|
|
static int running = 1;
|
|
|
|
/* engine state */
|
|
static Engine *g_engine = NULL;
|
|
static int active = 0;
|
|
static float bpm = 120.0f;
|
|
static int loop_length = 8; /* beats */
|
|
static int current_beat = 0;
|
|
|
|
/* Carla host and plugin URI input */
|
|
/* CarlaHost is now stored in Engine struct */
|
|
static char plugin_uri_input[256];
|
|
static int plugin_uri_input_len = 0;
|
|
static int selected_channel = 0;
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* zoom mode state
|
|
* ------------------------------------------------------------------------- */
|
|
static int zoom_enabled = 0; /* 0 = normal, 1 = zoomed */
|
|
static int zoom_level = 0; /* 0 = 1x, 1 = 2x, 2 = 4x */
|
|
static int zoom_focus_scene = 0; /* which scene is focused when zoomed */
|
|
static int zoom_focus_channel = 0; /* which channel is focused when zoomed */
|
|
static const int zoom_levels[] = {1, 2, 4};
|
|
static const char *zoom_labels[] = {"1x", "2x", "4x"};
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* drawing helpers (stubs for microui)
|
|
* ------------------------------------------------------------------------- */
|
|
static void draw_frame(mu_Context *ctx, mu_Rect rect, int colorid)
|
|
{
|
|
/* stub: we rely on ncurses for actual rendering */
|
|
(void)ctx;
|
|
(void)rect;
|
|
(void)colorid;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* GUI update
|
|
* ------------------------------------------------------------------------- */
|
|
static void gui_update(void)
|
|
{
|
|
mu_begin(ctx);
|
|
|
|
/* main window */
|
|
if (mu_begin_window(ctx, "Jack Looper", mu_rect(10, 10, 400, 300))) {
|
|
/* transport controls */
|
|
mu_layout_row(ctx, 3, (int[]) { 80, 80, 80 }, 0);
|
|
if (mu_button(ctx, active ? "Stop" : "Play")) {
|
|
active = !active;
|
|
if (active) {
|
|
Action action = { .type = ACTION_TRANSPORT_PLAY };
|
|
g_engine->dispatch(action);
|
|
} else {
|
|
Action action = { .type = ACTION_TRANSPORT_PAUSE };
|
|
g_engine->dispatch(action);
|
|
}
|
|
}
|
|
if (mu_button(ctx, "Reset")) {
|
|
Action action = { .type = ACTION_RESET_TRANSPORT };
|
|
g_engine->dispatch(action);
|
|
current_beat = 0;
|
|
}
|
|
if (mu_button(ctx, "Quit")) {
|
|
Action action = { .type = ACTION_QUIT };
|
|
g_engine->dispatch(action);
|
|
running = 0;
|
|
}
|
|
|
|
/* BPM slider */
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "BPM:");
|
|
mu_slider_ex(ctx, &bpm, 20.0f, 300.0f, 0, "%.0f", MU_OPT_ALIGNCENTER);
|
|
|
|
/* loop length */
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "Length:");
|
|
{
|
|
float loop_length_f = (float)loop_length;
|
|
mu_slider_ex(ctx, &loop_length_f, 1.0f, 64.0f, 0, "%.0f", 0);
|
|
loop_length = (int)loop_length_f;
|
|
}
|
|
|
|
/* beat indicator */
|
|
mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
|
|
{
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "Beat: %d / %d", current_beat + 1, loop_length);
|
|
mu_label(ctx, buf);
|
|
}
|
|
|
|
/* progress bar */
|
|
mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
|
|
{
|
|
float progress = (loop_length > 0) ? (float)current_beat / (float)loop_length : 0.0f;
|
|
mu_Rect r = mu_layout_next(ctx);
|
|
mu_Id prog_id = mu_get_id(ctx, &progress, sizeof(progress));
|
|
mu_draw_control_frame(ctx, prog_id, r, MU_COLOR_BASE, 0);
|
|
if (progress > 0.0f) {
|
|
mu_Rect fill = r;
|
|
fill.w = (int)(r.w * progress);
|
|
mu_draw_rect(ctx, fill, mu_color(100, 200, 100, 255));
|
|
}
|
|
}
|
|
|
|
/* zoom indicator */
|
|
mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
|
|
{
|
|
char buf[64];
|
|
if (zoom_enabled) {
|
|
snprintf(buf, sizeof(buf), "Zoom: %s Scene:%d Channel:%d",
|
|
zoom_labels[zoom_level],
|
|
zoom_focus_scene + 1,
|
|
zoom_focus_channel + 1);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "Zoom: Off");
|
|
}
|
|
mu_label(ctx, buf);
|
|
}
|
|
|
|
/* Zoom mode controls */
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "Zoom:");
|
|
if (mu_button(ctx, zoom_enabled ? "Disable" : "Enable")) {
|
|
zoom_enabled = !zoom_enabled;
|
|
if (!zoom_enabled) {
|
|
zoom_focus_scene = 0;
|
|
zoom_focus_channel = 0;
|
|
}
|
|
}
|
|
|
|
if (zoom_enabled) {
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "Level:");
|
|
if (mu_button(ctx, zoom_labels[zoom_level])) {
|
|
zoom_level = (zoom_level + 1) % 3;
|
|
zoom_focus_scene = 0;
|
|
zoom_focus_channel = 0;
|
|
}
|
|
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "Scene:");
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%d", zoom_focus_scene + 1);
|
|
mu_label(ctx, buf);
|
|
}
|
|
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "Channel:");
|
|
{
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%d", zoom_focus_channel + 1);
|
|
mu_label(ctx, buf);
|
|
}
|
|
|
|
/* Navigation hint */
|
|
mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
|
|
mu_label(ctx, "Use arrow keys to navigate when zoomed");
|
|
}
|
|
|
|
/* Plugin URI input */
|
|
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
|
mu_label(ctx, "URI:");
|
|
{
|
|
char display[256];
|
|
if (plugin_uri_input_len > 0) {
|
|
snprintf(display, sizeof(display), "%s_", plugin_uri_input);
|
|
} else {
|
|
snprintf(display, sizeof(display), "type URI here...");
|
|
}
|
|
mu_label(ctx, display);
|
|
}
|
|
mu_layout_row(ctx, 1, (int[]) { -1 }, 0);
|
|
if (mu_button(ctx, "Add Plugin")) {
|
|
if (plugin_uri_input_len > 0) {
|
|
Action action = { .type = ACTION_RACK_ADD_PLUGIN };
|
|
action.data.rack_add_plugin.channel = selected_channel;
|
|
strncpy(action.data.rack_add_plugin.uri, plugin_uri_input, sizeof(action.data.rack_add_plugin.uri) - 1);
|
|
action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0';
|
|
action.data.rack_add_plugin.type = PLUGIN_TYPE_INTERNAL;
|
|
g_engine->dispatch(action);
|
|
plugin_uri_input_len = 0;
|
|
plugin_uri_input[0] = '\0';
|
|
}
|
|
}
|
|
|
|
mu_end_window(ctx);
|
|
}
|
|
|
|
mu_end(ctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* main loop
|
|
* ------------------------------------------------------------------------- */
|
|
int gui_main(Engine *engine)
|
|
{
|
|
g_engine = engine;
|
|
|
|
/* Carla host is now initialized in engine_init */
|
|
|
|
/* initialise microui */
|
|
ctx = malloc(sizeof(mu_Context));
|
|
if (!ctx) return -1;
|
|
mu_init(ctx);
|
|
ctx->text_width = text_width;
|
|
ctx->text_height = text_height;
|
|
ctx->draw_frame = draw_frame;
|
|
|
|
/* ncurses setup */
|
|
initscr();
|
|
cbreak();
|
|
noecho();
|
|
keypad(stdscr, TRUE);
|
|
nodelay(stdscr, TRUE);
|
|
curs_set(0);
|
|
|
|
/* main loop */
|
|
while (running) {
|
|
/* handle input */
|
|
int ch = getch();
|
|
if (ch != ERR) {
|
|
/* Handle text input for plugin URI */
|
|
if (ch >= 32 && ch <= 126 && plugin_uri_input_len < 255) {
|
|
plugin_uri_input[plugin_uri_input_len++] = (char)ch;
|
|
plugin_uri_input[plugin_uri_input_len] = '\0';
|
|
} else if (ch == 127 || ch == KEY_BACKSPACE) {
|
|
if (plugin_uri_input_len > 0) {
|
|
plugin_uri_input[--plugin_uri_input_len] = '\0';
|
|
}
|
|
} else if (ch == '\n' || ch == '\r') {
|
|
/* Submit plugin URI */
|
|
if (plugin_uri_input_len > 0) {
|
|
Action action = { .type = ACTION_RACK_ADD_PLUGIN };
|
|
action.data.rack_add_plugin.channel = selected_channel;
|
|
strncpy(action.data.rack_add_plugin.uri, plugin_uri_input, sizeof(action.data.rack_add_plugin.uri) - 1);
|
|
action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0';
|
|
action.data.rack_add_plugin.type = PLUGIN_TYPE_INTERNAL;
|
|
g_engine->dispatch(action);
|
|
plugin_uri_input_len = 0;
|
|
plugin_uri_input[0] = '\0';
|
|
}
|
|
}
|
|
switch (ch) {
|
|
case 'q':
|
|
case 'Q':
|
|
running = 0;
|
|
break;
|
|
case ' ':
|
|
active = !active;
|
|
if (active) {
|
|
Action action = { .type = ACTION_TRANSPORT_PLAY };
|
|
g_engine->dispatch(action);
|
|
} else {
|
|
Action action = { .type = ACTION_TRANSPORT_PAUSE };
|
|
g_engine->dispatch(action);
|
|
}
|
|
break;
|
|
case 'z':
|
|
case 'Z':
|
|
zoom_enabled = !zoom_enabled;
|
|
if (!zoom_enabled) {
|
|
zoom_focus_scene = 0;
|
|
zoom_focus_channel = 0;
|
|
}
|
|
break;
|
|
case KEY_UP:
|
|
if (zoom_enabled) {
|
|
int step = zoom_levels[zoom_level];
|
|
zoom_focus_scene = (zoom_focus_scene - step + MAX_SCENES) % MAX_SCENES;
|
|
} else {
|
|
bpm = fminf(bpm + 5.0f, 300.0f);
|
|
}
|
|
break;
|
|
case KEY_DOWN:
|
|
if (zoom_enabled) {
|
|
int step = zoom_levels[zoom_level];
|
|
zoom_focus_scene = (zoom_focus_scene + step) % MAX_SCENES;
|
|
} else {
|
|
bpm = fmaxf(bpm - 5.0f, 20.0f);
|
|
}
|
|
break;
|
|
case KEY_LEFT:
|
|
if (zoom_enabled) {
|
|
int step = zoom_levels[zoom_level];
|
|
zoom_focus_channel = (zoom_focus_channel - step + MAX_CHANNELS) % MAX_CHANNELS;
|
|
} else {
|
|
loop_length = (loop_length > 1) ? loop_length - 1 : 1;
|
|
}
|
|
break;
|
|
case KEY_RIGHT:
|
|
if (zoom_enabled) {
|
|
int step = zoom_levels[zoom_level];
|
|
zoom_focus_channel = (zoom_focus_channel + step) % MAX_CHANNELS;
|
|
} else {
|
|
loop_length = (loop_length < 64) ? loop_length + 1 : 64;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* update engine state */
|
|
AppState state;
|
|
dispatcher_get_state(&state);
|
|
current_beat = state.bar_position * 4 + state.beat_position;
|
|
|
|
/* render GUI */
|
|
gui_update();
|
|
|
|
/* draw commands */
|
|
mu_Command *cmd = NULL;
|
|
while (mu_next_command(ctx, &cmd)) {
|
|
if (cmd->type == MU_COMMAND_TEXT) {
|
|
mvprintw(cmd->text.pos.y / 18, cmd->text.pos.x / 8, "%.*s",
|
|
(int)strlen(cmd->text.str), cmd->text.str);
|
|
}
|
|
if (cmd->type == MU_COMMAND_RECT) {
|
|
/* simple rectangle rendering using ncurses */
|
|
int x = cmd->rect.rect.x / 8;
|
|
int y = cmd->rect.rect.y / 18;
|
|
int w = cmd->rect.rect.w / 8;
|
|
int h = cmd->rect.rect.h / 18;
|
|
for (int i = 0; i < h && y + i < LINES; i++) {
|
|
mvhline(y + i, x, ' ', w);
|
|
}
|
|
}
|
|
}
|
|
|
|
refresh();
|
|
struct timespec ts = {0, 16666000}; /* 16666 us = 16666000 ns */
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
/* cleanup */
|
|
carla_cleanup(&g_engine->carla_host);
|
|
endwin();
|
|
free(ctx);
|
|
return 0;
|
|
}
|