feat: add scene-based recording, e2e tests, and improved TUI state indicators
This commit is contained in:
committed by
Loic Coenen (aider)
parent
f2993eac80
commit
7c289e1496
@@ -15,6 +15,7 @@ typedef enum {
|
||||
CMD_PREV_SCENE,
|
||||
CMD_ADD_SCENE,
|
||||
CMD_REMOVE_SCENE,
|
||||
CMD_SET_SCENE,
|
||||
} cmd_type_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* reading (consumer). No locks, no dynamic memory allocation.
|
||||
* Must be initialised before first use. All operations are RT‑safe. */
|
||||
|
||||
#define QUEUE_CAPACITY 256
|
||||
#define QUEUE_CAPACITY 1024
|
||||
|
||||
typedef struct {
|
||||
command_t buffer[QUEUE_CAPACITY];
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user