From 05c6f34b8ffd7da5940944da009b60af585f1708 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 1 May 2026 13:02:39 +0000 Subject: [PATCH] feat: add microui-based GUI with transport controls and progress bar Co-authored-by: aider (deepseek/deepseek-coder) --- docs/usage.md | 23 ++++++ engine.c | 111 +++++++++++++++++++++++++ gui.c | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++ gui.h | 8 ++ main.c | 44 ++++++++++ makefile | 16 +++- test_gui.c | 53 ++++++++++++ 7 files changed, 473 insertions(+), 3 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index a87344b..2a6857b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1741,3 +1741,26 @@ clicked increments that integer, could be implemented as such:

+# Usage + +## Overview + +The Jack Looper application provides a simple audio loop player using JACK. + +## Getting Started + +1. Ensure JACK is running +2. Run `./jack-looper` +3. Use the TUI interface to control playback + +## Controls + +- Space: Play/Stop +- Up/Down: Change BPM +- Left/Right: Change loop length +- R: Reset beat position +- Q: Quit + +## Building + +Run `make` to build all targets. diff --git a/engine.c b/engine.c index 9de91f2..378f93c 100644 --- a/engine.c +++ b/engine.c @@ -510,3 +510,114 @@ const char* quantize_mode_to_string(QuantizeMode mode) { default: return "Unknown"; } } +#include +#include +#include +#include +#include +#include "engine.h" + +static float *audio_buffer = NULL; +static int buffer_size = 0; +static float bpm = 120.0f; +static int loop_length = 8; +static int current_beat = 0; +static int active = 0; +static jack_nframes_t sample_rate = 0; +static jack_port_t *output_port = NULL; + +static int process_callback(jack_nframes_t nframes, void *arg) +{ + (void)arg; + jack_default_audio_sample_t *out = jack_port_get_buffer(output_port, nframes); + if (!out) return 0; + + /* simple metronome: generate a click on each beat */ + float samples_per_beat = sample_rate * 60.0f / bpm; + static float phase = 0.0f; + + for (jack_nframes_t i = 0; i < nframes; i++) { + if (phase >= samples_per_beat) { + phase -= samples_per_beat; + current_beat = (current_beat + 1) % loop_length; + } + + float sample = 0.0f; + if (active) { + /* short click at start of beat */ + float click_duration = samples_per_beat * 0.05f; + if (phase < click_duration) { + float t = phase / click_duration; + sample = sinf(t * M_PI) * 0.3f; + } + } + out[i] = sample; + phase += 1.0f; + } + + return 0; +} + +int engine_init(jack_client_t *client, float *buffer, int buf_size) +{ + audio_buffer = buffer; + buffer_size = buf_size; + + sample_rate = jack_get_sample_rate(client); + + output_port = jack_port_register(client, "output", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!output_port) { + fprintf(stderr, "no more JACK ports available\n"); + return -1; + } + + jack_set_process_callback(client, process_callback, NULL); + + if (jack_activate(client)) { + fprintf(stderr, "cannot activate client\n"); + return -1; + } + + return 0; +} + +void engine_cleanup(jack_client_t *client) +{ + jack_deactivate(client); + jack_port_unregister(client, output_port); +} + +int engine_start(jack_client_t *client) +{ + (void)client; + active = 1; + return 0; +} + +int engine_stop(jack_client_t *client) +{ + (void)client; + active = 0; + return 0; +} + +void engine_set_bpm(jack_client_t *client, float new_bpm) +{ + (void)client; + bpm = new_bpm; +} + +void engine_set_loop_length(jack_client_t *client, int beats) +{ + (void)client; + loop_length = beats; + if (current_beat >= loop_length) current_beat = 0; +} + +int engine_get_current_beat(jack_client_t *client) +{ + (void)client; + return current_beat; +} diff --git a/gui.c b/gui.c index e69de29..023714c 100644 --- a/gui.c +++ b/gui.c @@ -0,0 +1,221 @@ +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/gui.h b/gui.h index e69de29..8fb4063 100644 --- a/gui.h +++ b/gui.h @@ -0,0 +1,8 @@ +#ifndef GUI_H +#define GUI_H + +#include + +int gui_main(jack_client_t *client, float *audio_buffer, int buf_size); + +#endif /* GUI_H */ diff --git a/main.c b/main.c index 76f8838..0aa88b0 100644 --- a/main.c +++ b/main.c @@ -84,3 +84,47 @@ int main(int argc, char *argv[]) { return 0; } +#include +#include +#include +#include "engine.h" +#include "tui.h" + +int main(void) +{ + jack_client_t *client; + const char *client_name = "jack_looper"; + jack_options_t options = JackNullOption; + jack_status_t status; + + client = jack_client_open(client_name, options, &status); + if (client == NULL) { + fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); + if (status & JackServerFailed) { + fprintf(stderr, "Unable to connect to JACK server\n"); + } + return 1; + } + + float *buffer = calloc(4096, sizeof(float)); + if (!buffer) { + fprintf(stderr, "Failed to allocate audio buffer\n"); + jack_client_close(client); + return 1; + } + + if (engine_init(client, buffer, 4096) != 0) { + fprintf(stderr, "Failed to initialize engine\n"); + free(buffer); + jack_client_close(client); + return 1; + } + + int ret = tui_main(client, buffer, 4096); + + engine_cleanup(client); + free(buffer); + jack_client_close(client); + + return ret; +} diff --git a/makefile b/makefile index 73af288..608c6db 100644 --- a/makefile +++ b/makefile @@ -2,7 +2,7 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c99 -D_POSIX_C_SOURCE=200809L LDFLAGS = -ljack -lm -lncurses -all: jack-looper test_engine test_tui +all: jack-looper test_engine test_tui test_gui jack-looper: main.o engine.o tui.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) @@ -13,6 +13,9 @@ test_engine: test_engine.o engine.o test_tui: test_tui.o engine.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +test_gui: test_gui.o engine.o gui.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + main.o: main.c engine.h tui.h $(CC) $(CFLAGS) -c -o $@ $< @@ -22,17 +25,24 @@ engine.o: engine.c engine.h tui.o: tui.c tui.h engine.h $(CC) $(CFLAGS) -c -o $@ $< +gui.o: gui.c gui.h engine.h microui.h + $(CC) $(CFLAGS) -c -o $@ $< + test_engine.o: test_engine.c engine.h $(CC) $(CFLAGS) -c -o $@ $< test_tui.o: test_tui.c engine.h tui.h $(CC) $(CFLAGS) -c -o $@ $< +test_gui.o: test_gui.c engine.h gui.h + $(CC) $(CFLAGS) -c -o $@ $< + .PHONY: all clean test clean: - rm -f *.o jack-looper test_engine test_tui + rm -f *.o jack-looper test_engine test_tui test_gui -test: test_engine test_tui +test: test_engine test_tui test_gui ./test_engine ./test_tui + ./test_gui diff --git a/test_gui.c b/test_gui.c index e69de29..bb5f802 100644 --- a/test_gui.c +++ b/test_gui.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include "engine.h" +#include "gui.h" + +static jack_client_t *client = NULL; +static float *buffer = NULL; +static int buffer_size = 0; + +int main(void) +{ + /* create JACK client */ + const char *client_name = "jack_looper_gui_test"; + jack_options_t options = JackNullOption; + jack_status_t status; + + client = jack_client_open(client_name, options, &status); + if (client == NULL) { + fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); + if (status & JackServerFailed) { + fprintf(stderr, "Unable to connect to JACK server\n"); + } + return 1; + } + + /* allocate audio buffer */ + buffer_size = 4096; + buffer = calloc(buffer_size, sizeof(float)); + if (!buffer) { + fprintf(stderr, "Failed to allocate audio buffer\n"); + jack_client_close(client); + return 1; + } + + /* initialize engine */ + if (engine_init(client, buffer, buffer_size) != 0) { + fprintf(stderr, "Failed to initialize engine\n"); + free(buffer); + jack_client_close(client); + return 1; + } + + /* run GUI */ + int ret = gui_main(client, buffer, buffer_size); + + /* cleanup */ + engine_cleanup(client); + free(buffer); + jack_client_close(client); + + return ret; +}