feat: add direct JACK port connection and VU meter support

This commit is contained in:
Loic Coenen
2026-05-27 20:28:09 +00:00
committed by Loic Coenen (aider)
parent 1e62ec9310
commit 316320c294
12 changed files with 455 additions and 138 deletions

View File

@@ -58,6 +58,7 @@ struct channel_t {
_Atomic RingBuf *save_ring;
atomic_int save_complete; /* 1 when writer is done; RT thread must stop writing */
_Atomic float rms_level; /* RMS output level (computed in RT thread) */
};
/* Globals declared in looper.c */

View File

@@ -7,6 +7,7 @@
#include "queue.h"
#include "wav.h"
#include <fcntl.h>
#include <math.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
@@ -30,7 +31,7 @@ spsc_queue_t cmd_queue_main_fifo;
/* writer status fd */
static int status_fd = -1;
static jack_client_t *global_client = NULL;
jack_client_t *global_client = NULL;
/* Global state (shared across files) */
struct channel_t channels[MAX_CHANNELS];
@@ -105,9 +106,6 @@ static void looper_write_status(void) {
int sc_idx = atomic_load(&channels[ch].current_scene);
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
int prev = atomic_load(&prev_state[ch][sc_idx]);
if (state == prev)
continue; /* unchanged, skip */
atomic_store(&prev_state[ch][sc_idx], state);
const char *state_str;
switch (state) {
@@ -126,10 +124,22 @@ static void looper_write_status(void) {
default:
state_str = "UNKNOWN";
}
/* Always write state line to guarantee level line is sent even if state unchanged */
int n = snprintf(buf + pos, sizeof(buf) - pos,
"CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str);
if (n > 0) pos += n;
if (pos >= (int)sizeof(buf) - 128) break;
/* Write RMS level line every time (no change detection) */
{
float level = atomic_load(&channels[ch].rms_level);
int n2 = snprintf(buf + pos, sizeof(buf) - pos,
"CH=%d LEVEL=%f\n", ch, level);
if (n2 > 0) pos += n2;
if (pos >= (int)sizeof(buf) - 128) break;
}
atomic_store(&prev_state[ch][sc_idx], state);
}
if (pos > 0) {
int ret = write(status_fd, buf, pos);
@@ -455,6 +465,21 @@ int process_callback(jack_nframes_t nframes, void *arg) {
break;
}
/* Compute RMS level for this channel */
{
float sum_sq = 0.0f;
const float *f_out = (const float *)out;
for (jack_nframes_t i = 0; i < nframes; i++)
sum_sq += f_out[i] * f_out[i];
float rms = sqrtf(sum_sq / nframes);
atomic_store(&channels[c].rms_level, rms);
static float last_rms[MAX_CHANNELS] = {0};
if (rms > 0.001f && fabsf(rms - last_rms[c]) > 0.005f) {
fprintf(stderr, "RMS ch%d = %f\n", c, rms);
last_rms[c] = rms;
}
}
/* push loop output into save ring if saving (atomic load) */
RingBuf *r = (RingBuf *)atomic_load_explicit(&channels[c].save_ring,
memory_order_acquire);
@@ -552,9 +577,6 @@ int looper_init(jack_client_t *client) {
queue_init(&cmd_queue_main_midi);
queue_init(&cmd_queue_main_fifo);
/* start the FIFO reader thread */
pipe_start_reader();
/* channel 0 */
channels[0].active = 1;
channels[0].type = CHANNEL_AUDIO; /* default */
@@ -592,6 +614,9 @@ int looper_init(jack_client_t *client) {
nanosleep(&req, NULL);
}
/* start the FIFO reader thread (after ports are registered) */
pipe_start_reader();
return 0;
}
@@ -648,8 +673,8 @@ void looper_process_commands(jack_client_t *client) {
if (atomic_exchange(&cmd_load, 0)) {
float *buf = NULL;
unsigned frames = 0;
fprintf(stderr, "LOAD: wav_read called\n");
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
fprintf(stderr, "LOAD: wav_read called for %s\n", load_filename);
if (wav_read(load_filename, &buf, &frames) == 0 && frames > 0) {
fprintf(stderr, "LOAD: success, frames=%u\n", frames);
int sc_idx = atomic_load(&channels[0].current_scene);
scene_t *sc = &channels[0].scenes[sc_idx];
@@ -663,7 +688,7 @@ void looper_process_commands(jack_client_t *client) {
atomic_store(&sc->prev_state, -1);
free(buf);
} else {
fprintf(stderr, "Failed to load loop.wav\n");
fprintf(stderr, "Failed to load %s\n", load_filename);
fprintf(stderr, "LOAD: FAILED\n");
}
}

View File

@@ -4,6 +4,8 @@
// cppcheck-suppress missingIncludeSystem
#include <jack/jack.h>
extern jack_client_t *global_client;
/* Initialisation must be called after setting process callback */
int looper_init(jack_client_t *client);

View File

@@ -10,10 +10,20 @@
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <jack/jack.h>
#define FIFO_PATH "/tmp/looper_cmd"
#define LINE_MAX 256
/* Global JACK client (from looper.c) */
extern jack_client_t *global_client;
/* Stored ports for from/to */
static char fifo_from[256] = "";
static char fifo_to[256] = "";
/* Filename for the next load command (default "loop.wav") */
char load_filename[256] = "loop.wav";
/* forwarddeclare the global queues (defined in looper.c) */
extern spsc_queue_t cmd_queue;
extern spsc_queue_t cmd_queue_main_fifo;
@@ -78,13 +88,92 @@ static void *pipe_thread_func(void *arg) {
command_t cmd = {.type = CMD_SET_SCENE, .channel = ch, .data = sc};
queue_push(&cmd_queue_main_fifo, cmd);
}
} else if (strcmp(line, "load") == 0) {
} else if (strncmp(line, "load", 4) == 0) {
/* Parse optional filename after "load " */
const char *fn = line + 4;
while (*fn == ' ') fn++;
if (*fn == '\0') {
strncpy(load_filename, "loop.wav", sizeof(load_filename) - 1);
} else {
strncpy(load_filename, fn, sizeof(load_filename) - 1);
}
load_filename[sizeof(load_filename) - 1] = '\0';
fprintf(stderr, "FIFO RECEIVED load: %s\n", load_filename);
command_t cmd = {.type = CMD_LOAD, .channel = -1, .data = 0};
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "save") == 0) {
command_t cmd = {.type = CMD_SAVE, .channel = -1, .data = 0};
queue_push(&cmd_queue_main_fifo, cmd);
}
// --- from <port> ---
else if (strncmp(line, "from ", 5) == 0) {
fprintf(stderr, "FIFO RECEIVED from: %s\n", line + 5);
strncpy(fifo_from, line + 5, sizeof(fifo_from)-1);
fifo_from[sizeof(fifo_from)-1] = '\0';
// Immediately connect source to looper:input (independently of :to)
if (global_client) {
const char *target = "looper:input";
int ret = jack_connect(global_client, fifo_from, target);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", fifo_from, target, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, fifo_from, target);
if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", fifo_from, target, ret);
}
}
}
// --- to <port> ---
else if (strncmp(line, "to ", 3) == 0) {
fprintf(stderr, "FIFO RECEIVED to: %s\n", line + 3);
strncpy(fifo_to, line + 3, sizeof(fifo_to)-1);
fifo_to[sizeof(fifo_to)-1] = '\0';
// Immediately connect looper:output to target (independently of :from)
if (global_client) {
const char *source = "looper:output";
int ret = jack_connect(global_client, source, fifo_to);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", source, fifo_to, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, source, fifo_to);
if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", source, fifo_to, ret);
}
}
}
// --- connect [from] [to] ---
else if (strncmp(line, "connect", 7) == 0) {
char from[256] = "";
char to[256] = "";
// parse optional arguments: "connect from to"
char *p = line + 7;
while (*p == ' ') p++;
if (*p) {
char *space = strchr(p, ' ');
if (space) {
strncpy(from, p, space - p); from[space-p] = '\0';
strncpy(to, space+1, sizeof(to)-1);
} else {
strncpy(from, p, sizeof(from)-1);
}
}
// fallback to stored ports
if (!from[0]) strncpy(from, fifo_from, sizeof(from)-1);
if (!to[0]) strncpy(to, fifo_to, sizeof(to)-1);
if (from[0] && to[0] && global_client) {
int ret = jack_connect(global_client, from, to);
if (ret != 0) {
fprintf(stderr, "Failed to connect %s -> %s (ret=%d), retrying...\n", from, to, ret);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 500000000};
nanosleep(&ts, NULL);
ret = jack_connect(global_client, from, to);
if (ret != 0)
fprintf(stderr, "Retry also failed %s -> %s (ret=%d)\n", from, to, ret);
}
}
}
/* ignore unknown lines */
}
/* EOF all writers closed, reopen for next connection */

View File

@@ -6,4 +6,7 @@
* Returns 0 on success, -1 on failure. */
int pipe_start_reader(void);
/** Filename for the next load command (default "loop.wav") */
extern char load_filename[256];
#endif