feat: add JACK port management, VU meter, fzf integration, and e2e tests

This commit is contained in:
Loic Coenen
2026-06-06 13:32:20 +00:00
committed by Loic Coenen (aider)
30 changed files with 2303 additions and 177 deletions

View File

@@ -1,6 +1,6 @@
CC ?= gcc
CFLAGS ?= -Wall -Wextra -g -Isrc
LDFLAGS ?= -ljack -lm -lsndfile -lpthread
CFLAGS ?= -Wall -Wextra -g -Isrc -fsanitize=address -fno-omit-frame-pointer
LDFLAGS ?= -fsanitize=address -ljack -lm -lsndfile -lpthread
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c src/ringbuffer.c src/wav.c src/log.c
OBJ = $(SRC:.c=.o)

View File

@@ -4,6 +4,7 @@
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/* Helper: zero a scene and set its state to IDLE */
void init_scene(scene_t *sc) {
@@ -14,8 +15,9 @@ void init_scene(scene_t *sc) {
void channel_add(jack_client_t *client, int idx) {
char in_name[64], out_name[64];
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
pid_t pid = getpid();
snprintf(in_name, sizeof(in_name), "ch%din", next_channel_id);
snprintf(out_name, sizeof(out_name), "ch%dout", next_channel_id);
/* Always register audio ports (needed for pass-through even for MIDI
* channels?) */
@@ -34,9 +36,8 @@ void channel_add(jack_client_t *client, int idx) {
/* If this is a MIDI channel, register MIDI ports */
if (channels[idx].type == CHANNEL_MIDI) {
char midi_in_name[64], midi_out_name[64];
snprintf(midi_in_name, sizeof(midi_in_name), "channel%d_midi_in",
next_channel_id);
snprintf(midi_out_name, sizeof(midi_out_name), "channel%d_midi_out",
snprintf(midi_in_name, sizeof(midi_in_name), "ch%dmidiin", next_channel_id);
snprintf(midi_out_name, sizeof(midi_out_name), "ch%dmidiout",
next_channel_id);
channels[idx].midi_in = jack_port_register(
client, midi_in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
@@ -70,8 +71,8 @@ void channel_add(jack_client_t *client, int idx) {
void channel_remove(jack_client_t *client, int idx) {
(void)client;
atomic_store(&channels[idx].active, 0);
atomic_fetch_sub(&channel_count, 1);
atomic_store_explicit(&channels[idx].active, 0, memory_order_release);
atomic_fetch_sub_explicit(&channel_count, 1, memory_order_release);
}
void channel_add_scene(jack_client_t *client, int idx) {

View File

@@ -5,7 +5,7 @@
#include <jack/jack.h>
#include <stdatomic.h>
#define MAX_SCENES 4
#define MAX_SCENES 8
#define LOOP_BUF_SIZE (5 * 48000)
#define MAX_MIDI_EVENTS 1024
#define MAX_CHANNELS 16
@@ -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

@@ -15,6 +15,7 @@ typedef enum {
CMD_PREV_SCENE,
CMD_ADD_SCENE,
CMD_REMOVE_SCENE,
CMD_SET_SCENE,
} cmd_type_t;
typedef struct {

View File

@@ -1,32 +1,33 @@
#include "log.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
static FILE *logfile = NULL;
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
void log_init(void) {
logfile = fopen("/tmp/looper.log", "a");
if (!logfile)
logfile = stderr;
setbuf(logfile, NULL);
logfile = fopen("./looper.log", "a");
if (!logfile)
logfile = stderr;
setbuf(logfile, NULL);
}
void log_msg(const char *fmt, ...) {
if (!logfile) return;
pthread_mutex_lock(&log_mutex);
va_list args;
va_start(args, fmt);
vfprintf(logfile, fmt, args);
va_end(args);
fputc('\n', logfile);
pthread_mutex_unlock(&log_mutex);
if (!logfile)
return;
pthread_mutex_lock(&log_mutex);
va_list args;
va_start(args, fmt);
vfprintf(logfile, fmt, args);
va_end(args);
fputc('\n', logfile);
pthread_mutex_unlock(&log_mutex);
}
void log_close(void) {
if (logfile && logfile != stderr)
fclose(logfile);
logfile = NULL;
if (logfile && logfile != stderr)
fclose(logfile);
logfile = NULL;
}

View File

@@ -1,15 +1,18 @@
// cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include "channel.h"
#include "log.h"
#include "midi.h"
#include "pipe.h"
#include "queue.h"
#include "wav.h"
#include <errno.h>
#include <fcntl.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
@@ -27,15 +30,28 @@ spsc_queue_t cmd_queue_main_fifo;
/* writer status fd */
static int status_fd = -1;
/* global sample rate (set during init) */
static int global_sample_rate = 0;
/* global JACK client pointer used by channel.c */
jack_client_t *global_client = NULL;
/* default filename for load/save */
/* ---------- prev_state moved before first user ---------- */
static atomic_int prev_state[MAX_CHANNELS][MAX_SCENES];
static void looper_write_status(void) {
if (status_fd < 0)
return;
char buf[256];
char buf[4096];
int pos = 0;
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
if (!atomic_load(&channels[ch].active))
continue;
int sc_idx = atomic_load(&channels[ch].current_scene);
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
const char *state_str;
switch (state) {
case STATE_IDLE:
@@ -53,16 +69,31 @@ static void looper_write_status(void) {
default:
state_str = "UNKNOWN";
}
int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx,
state_str);
if (n > 0) {
int ret = write(status_fd, buf, n);
(void)ret;
/* Always write state line */
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 */
{
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;
}
}
if (pos > 0) {
int ret = write(status_fd, buf, pos);
(void)ret;
}
}
/* Global state (shared across files) */
struct channel_t channels[MAX_CHANNELS];
atomic_int channel_count = 0;
atomic_int channel_capacity = MAX_CHANNELS;
@@ -76,11 +107,48 @@ jack_port_t *midi_clock_port = NULL;
atomic_int control_key_active = 0;
atomic_int bind_channel = 0;
/* Deferred removal index (1 second grace) */
static int pending_unregister_idx = -1;
static void looper_cleanup(jack_client_t *client) {
for (int c = 0; c < MAX_CHANNELS; c++) {
if (channels[c].audio_in) {
jack_port_unregister(client, channels[c].audio_in);
channels[c].audio_in = NULL;
}
if (channels[c].audio_out) {
jack_port_unregister(client, channels[c].audio_out);
channels[c].audio_out = NULL;
}
if (channels[c].midi_in) {
jack_port_unregister(client, channels[c].midi_in);
channels[c].midi_in = NULL;
}
if (channels[c].midi_out) {
jack_port_unregister(client, channels[c].midi_out);
channels[c].midi_out = NULL;
}
}
if (midi_control_port) {
jack_port_unregister(client, midi_control_port);
midi_control_port = NULL;
}
if (midi_clock_port) {
jack_port_unregister(client, midi_clock_port);
midi_clock_port = NULL;
}
}
/* sample rate holder */
static int global_sample_rate = 0;
void looper_shutdown(jack_client_t *client) {
jack_deactivate(client);
looper_cleanup(client);
jack_client_close(client);
log_close();
}
volatile int looper_quit = 0;
static void signal_handler(int sig) {
(void)sig;
looper_quit = 1;
}
static int pending_unregister_idx = -1;
/* execute a single command (called from looper_process_commands) */
static void exec_command(command_t cmd, jack_client_t *client) {
@@ -90,9 +158,40 @@ static void exec_command(command_t cmd, jack_client_t *client) {
switch (cmd.type) {
case CMD_CYCLE: {
int ch = cmd.channel;
if (ch < 0 || ch >= MAX_CHANNELS)
ch = 0;
// Save the desired scene (may have been set by CMD_SET_SCENE)
int requested_scene = atomic_load(&channels[ch].current_scene);
// Clamp requested_scene to valid range
if (requested_scene < 0)
requested_scene = 0;
if (requested_scene >= MAX_SCENES)
requested_scene = MAX_SCENES - 1;
// Auto-create channel if it doesn't exist
if (!channels[ch].active) {
channel_add(client, ch);
}
// Ensure enough scenes exist to satisfy requested_scene
int sc_count = atomic_load(&channels[ch].scene_count);
while (requested_scene >= sc_count && sc_count < MAX_SCENES) {
channel_add_scene(client, ch);
sc_count = atomic_load(&channels[ch].scene_count);
}
// Clamp requested_scene if MAX_SCENES prevents adding enough scenes
if (requested_scene >= sc_count)
requested_scene = sc_count - 1;
// Restore the requested scene (channel_add or add_scene may have reset
// current_scene)
atomic_store(&channels[ch].current_scene, requested_scene);
int sc_idx = atomic_load(&channels[ch].current_scene);
scene_t *sc_ptr = &channels[ch].scenes[sc_idx];
int state = atomic_load(&sc_ptr->state);
fprintf(stderr, "CMD_CYCLE: ch=%d old_state=%d\n", ch, state);
switch (state) {
case STATE_IDLE:
atomic_store(&sc_ptr->state, STATE_RECORD);
@@ -171,6 +270,15 @@ static void exec_command(command_t cmd, jack_client_t *client) {
channel_prev_scene(client, ch);
break;
case CMD_SET_SCENE: {
int sc = cmd.data;
// Allow any scene index; scenes will be added by CMD_CYCLE if needed
if (sc >= 0) {
atomic_store(&channels[ch].current_scene, sc);
}
break;
}
default:
break;
}
@@ -301,8 +409,19 @@ int process_callback(jack_nframes_t nframes, void *arg) {
if (!out)
continue;
if (c == 0 && !atomic_load(&channels[c].active)) {
fprintf(stderr, "CHANNEL0_NOT_ACTIVE during record\n");
}
switch (state) {
case STATE_RECORD:
if (c == 0 && atomic_load(&sc->record_pos) == 0) {
if (in) {
fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float *)in)[0]);
} else {
fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n");
}
}
if (in) {
float *f_out = (float *)out;
const float *f_in = (const float *)in;
@@ -346,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);
@@ -417,23 +551,32 @@ int looper_init(jack_client_t *client) {
/* store sample rate for writer thread */
global_sample_rate = jack_get_sample_rate(client);
global_client = client;
/* Install signal handlers for graceful shutdown */
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGQUIT, signal_handler);
/* create status FIFO (ignore if already exists) */
mkfifo(STATUS_FIFO, 0666);
/* open the status FIFO for reading+writing so writes work even without reader
*/
status_fd = open(STATUS_FIFO, O_RDWR);
status_fd = open(STATUS_FIFO, O_RDWR | O_NONBLOCK);
if (status_fd < 0) {
perror("open status FIFO");
}
/* initialise prev_state to -1 */
for (int ch = 0; ch < MAX_CHANNELS; ch++)
for (int sc = 0; sc < MAX_SCENES; sc++)
atomic_init(&prev_state[ch][sc], -1);
queue_init(&cmd_queue);
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 */
@@ -447,9 +590,9 @@ int looper_init(jack_client_t *client) {
atomic_store(&channels[0].save_complete, 0);
channels[0].audio_in = jack_port_register(
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
client, "ch0in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
channels[0].audio_out = jack_port_register(
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
client, "ch0out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
if (!channels[0].audio_in || !channels[0].audio_out) {
fprintf(stderr, "Could not create audio ports for channel 0\n");
return -1;
@@ -471,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;
}
@@ -527,9 +673,9 @@ void looper_process_commands(jack_client_t *client) {
if (atomic_exchange(&cmd_load, 0)) {
float *buf = NULL;
unsigned frames = 0;
printf("LOAD: wav_read called\n");
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
printf("LOAD: success, frames=%u\n", frames);
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];
if (frames > LOOP_BUF_SIZE)
@@ -542,8 +688,8 @@ 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");
printf("LOAD: FAILED\n");
fprintf(stderr, "Failed to load %s\n", load_filename);
fprintf(stderr, "LOAD: FAILED\n");
}
}
@@ -552,7 +698,17 @@ void looper_process_commands(jack_client_t *client) {
int sc_idx = atomic_load(&channels[0].current_scene);
scene_t *sc = &channels[0].scenes[sc_idx];
int lc = atomic_load(&sc->loop_count);
if (atomic_load(&sc->state) == STATE_LOOPING && lc > 0) {
int rp = atomic_load(&sc->record_pos);
int state = atomic_load(&sc->state);
printf("SAVE debug: state=%d loop_count=%d record_pos=%d\n", state, lc, rp);
/* Allow save from any state where we have data */
int frames_to_save = 0;
if ((state == STATE_LOOPING || state == STATE_PAUSED) && lc > 0) {
frames_to_save = lc;
} else if (state == STATE_RECORD && rp > 0) {
frames_to_save = rp;
}
if (frames_to_save > 0) {
/* Deactivate channel to prevent RT thread from reading the buffer */
int was_active = atomic_load(&channels[0].active);
if (was_active) {
@@ -561,13 +717,19 @@ void looper_process_commands(jack_client_t *client) {
nanosleep(&req, NULL);
}
/* Now safe to copy the loop buffer */
float *data = malloc((size_t)lc * sizeof(float));
float *data = malloc((size_t)frames_to_save * sizeof(float));
if (data) {
memcpy(data, sc->loop.audio_buffer, (size_t)lc * sizeof(float));
memcpy(data, sc->loop.audio_buffer,
(size_t)frames_to_save * sizeof(float));
unsigned sr = (unsigned)global_sample_rate;
if (sr == 0)
sr = 48000;
wav_write("save.wav", data, (unsigned)lc, sr);
char save_path[256];
snprintf(save_path, sizeof(save_path), "save.wav");
printf("SAVE: writing %u frames, first sample = %f\n",
(unsigned)frames_to_save, data[0]);
int ret = wav_write(save_path, data, (unsigned)frames_to_save, sr);
printf("SAVE: wav_write returned %d\n", ret);
free(data);
}
/* Reactivate channel use a shorter sleep to reduce xrun risk */
@@ -576,6 +738,8 @@ void looper_process_commands(jack_client_t *client) {
nanosleep(&req, NULL);
atomic_store(&channels[0].active, 1);
}
} else {
printf("SAVE: condition not met, state=%d lc=%d rp=%d\n", state, lc, rp);
}
}

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);
@@ -16,4 +18,10 @@ void jack_shutdown_cb(void *arg);
/* Mainloop command processing (add/remove channels) */
void looper_process_commands(jack_client_t *client);
/* Shutdown (must be called from the main thread after looper_quit is set) */
void looper_shutdown(jack_client_t *client);
/* Flag set by signal handler main loop should check this */
extern volatile int looper_quit;
#endif

View File

@@ -1,6 +1,7 @@
// cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include "log.h"
#include "looper.h"
#include "pipe.h"
#include <jack/jack.h>
#include <stdio.h>
#include <stdlib.h>
@@ -47,17 +48,23 @@ int main(int argc, char *argv[]) {
return 1;
}
if (pipe_start_reader() != 0) {
log_msg("pipe_start_reader() failed");
jack_client_close(client);
log_close();
return 1;
}
log_msg("looper running (client name '%s')", client_name);
while (1) {
while (!looper_quit) {
looper_process_commands(client);
{
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
struct timespec ts = {.tv_sec = 0, .tv_nsec = 10000000};
nanosleep(&ts, NULL);
}
}
jack_client_close(client);
log_close();
looper_shutdown(client);
return 0;
}

View File

@@ -3,6 +3,7 @@
#include "queue.h"
#include <errno.h>
#include <fcntl.h>
#include <jack/jack.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
@@ -11,9 +12,14 @@
#include <time.h>
#include <unistd.h>
extern jack_client_t *global_client;
#define FIFO_PATH "/tmp/looper_cmd"
#define LINE_MAX 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;
@@ -37,55 +43,87 @@ static void *pipe_thread_func(void *arg) {
if (strcmp(line, "add") == 0) {
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "add_midi") == 0) {
command_t cmd = {
.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "remove") == 0) {
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strncmp(line, "record ", 7) == 0) {
int ch = atoi(line + 7);
fprintf(stderr, "FIFO: received record %d\n", ch);
command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "stop") == 0) {
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strncmp(line, "bind ", 5) == 0) {
int ch = atoi(line + 5);
command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "unbind") == 0) {
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "scene_add") == 0) {
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "scene_remove") == 0) {
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "scene_next") == 0) {
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strcmp(line, "scene_prev") == 0) {
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
queue_push(&cmd_queue, cmd);
} else if (strcmp(line, "load") == 0) {
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strncmp(line, "set_scene ", 10) == 0) {
int ch, sc;
if (sscanf(line + 10, "%d %d", &ch, &sc) == 2) {
command_t cmd = {.type = CMD_SET_SCENE, .channel = ch, .data = sc};
queue_push(&cmd_queue_main_fifo, cmd);
}
} 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, cmd);
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, cmd);
queue_push(&cmd_queue_main_fifo, cmd);
} else if (strncmp(line, "from ", 5) == 0) {
const char *port = line + 5;
fprintf(stderr, "FIFO RECEIVED from: %s\n", port);
if (global_client) {
int ret = jack_connect(global_client, port, "looper:ch0in");
if (ret != 0)
fprintf(stderr, "Failed to connect %s -> looper:ch0in (ret=%d)\n",
port, ret);
}
} else if (strncmp(line, "to ", 3) == 0) {
const char *port = line + 3;
fprintf(stderr, "FIFO RECEIVED to: %s\n", port);
if (global_client) {
int ret = jack_connect(global_client, "looper:ch0out", port);
if (ret != 0)
fprintf(stderr, "Failed to connect looper:ch0out -> %s (ret=%d)\n",
port, ret);
}
}
/* ignore unknown lines */
}
/* EOF all writers closed, reopen for next connection */
fclose(fifo);
{
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
nanosleep(&ts, NULL);
} /* small pause before retrying */
}
return NULL; /* unreachable */
}

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

View File

@@ -10,7 +10,7 @@
* reading (consumer). No locks, no dynamic memory allocation.
* Must be initialised before first use. All operations are RTsafe. */
#define QUEUE_CAPACITY 256
#define QUEUE_CAPACITY 1024
typedef struct {
command_t buffer[QUEUE_CAPACITY];

View File

@@ -1018,17 +1018,19 @@ static int test_wav_save(void) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
/* FIFO: record channel 0, then stop to create a loop */
/* Use FIFO command to start recording */
if (send_fifo_command("record 0") != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000);
/* start generating a beep */
/* Set up beep generation for 3 seconds */
int sr = jack_get_sample_rate(client);
continuous_sine = 0;
beep_remaining = (int)(0.5f * sr);
beep_remaining = (int)(3.0f * sr);
bursts = 0; prev_above = 0;
passthrough_output_port = audio_out;
passthrough_input_port = audio_in;
@@ -1040,23 +1042,23 @@ static int test_wav_save(void) {
passthrough_done = 0;
jack_set_process_callback(client, passthrough_process, NULL);
if (jack_activate(client)) {
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(3000000); /* record for 3s (ensure enough beep) */
/* Send second record command to transition RECORD → LOOPING */
/* Second FIFO command to transition RECORD → LOOPING */
if (send_fifo_command("record 0") != 0) {
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(1000000); /* give time for state change and loop_count to be set */
safe_usleep(3000000); /* give time for state change and loop_count to be set */
/* save */
/* save via FIFO command */
if (send_fifo_command("save") != 0) {
jack_deactivate(client);
jack_client_close(client);