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:
Loic Coenen
2026-05-01 13:02:39 +00:00
parent 3b95c22736
commit 05c6f34b8f
7 changed files with 473 additions and 3 deletions

View File

@@ -1741,3 +1741,26 @@ clicked increments that integer, could be implemented as such:</p>
</body>
</html>
# 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.

111
engine.c
View File

@@ -510,3 +510,114 @@ const char* quantize_mode_to_string(QuantizeMode mode) {
default: return "Unknown";
}
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <jack/jack.h>
#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;
}

221
gui.c
View File

@@ -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;
}

8
gui.h
View File

@@ -0,0 +1,8 @@
#ifndef GUI_H
#define GUI_H
#include <jack/jack.h>
int gui_main(jack_client_t *client, float *audio_buffer, int buf_size);
#endif /* GUI_H */

44
main.c
View File

@@ -84,3 +84,47 @@ int main(int argc, char *argv[]) {
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <jack/jack.h>
#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;
}

View File

@@ -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

View File

@@ -0,0 +1,53 @@
#include <stdio.h>
#include <stdlib.h>
#include <jack/jack.h>
#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;
}