feat: add scene-based recording, e2e tests, and improved TUI state indicators

This commit is contained in:
Loic Coenen
2026-05-22 17:52:13 +00:00
committed by Loic Coenen (aider)
parent f2993eac80
commit 7c289e1496
11 changed files with 1024 additions and 37 deletions

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

@@ -90,9 +90,33 @@ 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);
// Auto-create channel if it doesn't exist
if (!channels[ch].active) {
channel_add(client, ch);
// Add scenes up to the requested scene
int sc_count = atomic_load(&channels[ch].scene_count);
while (sc_count <= requested_scene) {
channel_add_scene(client, ch);
sc_count = atomic_load(&channels[ch].scene_count);
}
// Restore the requested scene (channel_add resets to 0)
atomic_store(&channels[ch].current_scene, requested_scene);
// Give JACK time to register ports
struct timespec req = {.tv_sec = 0, .tv_nsec = 200000000};
nanosleep(&req, NULL);
}
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 +195,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 setting the scene even if channel is not yet active
if (sc >= 0 && sc < MAX_SCENES) {
atomic_store(&channels[ch].current_scene, sc);
}
break;
}
default:
break;
}
@@ -301,8 +334,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;
@@ -527,9 +571,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");
fprintf(stderr, "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: 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)
@@ -543,7 +587,7 @@ void looper_process_commands(jack_client_t *client) {
free(buf);
} else {
fprintf(stderr, "Failed to load loop.wav\n");
printf("LOAD: FAILED\n");
fprintf(stderr, "LOAD: FAILED\n");
}
}
@@ -552,7 +596,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,12 +615,16 @@ 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 */
@@ -575,6 +633,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

@@ -37,55 +37,58 @@ 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);
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 (strcmp(line, "load") == 0) {
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);
}
/* 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

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