feat: add microui-based GUI with transport controls and progress bar
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
221
gui.c
221
gui.c
@@ -0,0 +1,221 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <jack/jack.h>
|
||||
#include <ncurses.h>
|
||||
#include "engine.h"
|
||||
#include "gui.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 jack_client_t *client = NULL;
|
||||
static int active = 0;
|
||||
static float bpm = 120.0f;
|
||||
static int loop_length = 8; /* beats */
|
||||
static int current_beat = 0;
|
||||
static float *buffer = NULL;
|
||||
static int buffer_size = 0;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 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) {
|
||||
engine_start(client);
|
||||
} else {
|
||||
engine_stop(client);
|
||||
}
|
||||
}
|
||||
if (mu_button(ctx, "Reset")) {
|
||||
current_beat = 0;
|
||||
}
|
||||
if (mu_button(ctx, "Quit")) {
|
||||
running = 0;
|
||||
}
|
||||
|
||||
/* BPM slider */
|
||||
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
||||
mu_label(ctx, "BPM:");
|
||||
if (mu_slider(ctx, &bpm, 20.0f, 300.0f, 0, "%.0f", 0)) {
|
||||
engine_set_bpm(client, bpm);
|
||||
}
|
||||
|
||||
/* loop length */
|
||||
mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0);
|
||||
mu_label(ctx, "Length:");
|
||||
if (mu_slider(ctx, (float*)&loop_length, 1.0f, 64.0f, 0, "%.0f", 0)) {
|
||||
engine_set_loop_length(client, loop_length);
|
||||
}
|
||||
|
||||
/* 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_draw_control_frame(ctx, mu_get_id(ctx, &progress, sizeof(progress)), 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));
|
||||
}
|
||||
}
|
||||
|
||||
mu_end_window(ctx);
|
||||
}
|
||||
|
||||
mu_end(ctx);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* main loop
|
||||
* ------------------------------------------------------------------------- */
|
||||
int gui_main(jack_client_t *jack_client, float *audio_buffer, int buf_size)
|
||||
{
|
||||
client = jack_client;
|
||||
buffer = audio_buffer;
|
||||
buffer_size = buf_size;
|
||||
|
||||
/* 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) {
|
||||
switch (ch) {
|
||||
case 'q':
|
||||
case 'Q':
|
||||
running = 0;
|
||||
break;
|
||||
case ' ':
|
||||
active = !active;
|
||||
if (active) {
|
||||
engine_start(client);
|
||||
} else {
|
||||
engine_stop(client);
|
||||
}
|
||||
break;
|
||||
case KEY_UP:
|
||||
bpm = fminf(bpm + 5.0f, 300.0f);
|
||||
engine_set_bpm(client, bpm);
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
bpm = fmaxf(bpm - 5.0f, 20.0f);
|
||||
engine_set_bpm(client, bpm);
|
||||
break;
|
||||
case KEY_LEFT:
|
||||
loop_length = (loop_length > 1) ? loop_length - 1 : 1;
|
||||
engine_set_loop_length(client, loop_length);
|
||||
break;
|
||||
case KEY_RIGHT:
|
||||
loop_length = (loop_length < 64) ? loop_length + 1 : 64;
|
||||
engine_set_loop_length(client, loop_length);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* update engine state */
|
||||
current_beat = engine_get_current_beat(client);
|
||||
|
||||
/* 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",
|
||||
cmd->text.len, cmd->text.text);
|
||||
}
|
||||
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();
|
||||
usleep(16666); /* ~60 fps */
|
||||
}
|
||||
|
||||
/* cleanup */
|
||||
endwin();
|
||||
free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user