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

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