From 7b00246443106c57fd88737ed15c1376ba0961fb Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 18:00:32 +0000 Subject: [PATCH] feat: implement scene infrastructure for multi-scene looper support Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 67 ++++++++++++++--- src/channel.h | 29 +++++--- src/command.h | 8 ++- src/looper.c | 196 +++++++++++++++++++++++++++++++++++--------------- src/midi.c | 20 ++++++ src/pipe.c | 12 ++++ 6 files changed, 254 insertions(+), 78 deletions(-) diff --git a/src/channel.c b/src/channel.c index f6f6757..acab6c8 100644 --- a/src/channel.c +++ b/src/channel.c @@ -5,6 +5,13 @@ #include #include +/* Helper: zero a scene and set its state to IDLE */ +static void init_scene(scene_t *sc) { + memset(sc, 0, sizeof(scene_t)); + atomic_store(&sc->state, STATE_IDLE); + sc->prev_state = -1; +} + void channel_add(jack_client_t *client, int idx) { struct channel_t *cur = get_channels_array(); @@ -24,12 +31,10 @@ void channel_add(jack_client_t *client, int idx) { } atomic_store(&cur[idx].active, 1); - atomic_store(&cur[idx].state, STATE_IDLE); - cur[idx].prev_state = -1; - cur[idx].loop_count = 0; - cur[idx].record_pos = 0; - cur[idx].playback_pos = 0; cur[idx].type = CHANNEL_AUDIO; + cur[idx].scene_count = 1; + cur[idx].current_scene = 0; + init_scene(&cur[idx].scenes[0]); next_channel_id++; atomic_fetch_add(&channel_count, 1); @@ -54,12 +59,10 @@ void channel_add_midi(jack_client_t *client, int idx) { } atomic_store(&cur[idx].active, 1); - atomic_store(&cur[idx].state, STATE_IDLE); - cur[idx].prev_state = -1; - cur[idx].loop_count = 0; - cur[idx].record_pos = 0; - cur[idx].playback_pos = 0; cur[idx].type = CHANNEL_MIDI; + cur[idx].scene_count = 1; + cur[idx].current_scene = 0; + init_scene(&cur[idx].scenes[0]); next_channel_id++; atomic_fetch_add(&channel_count, 1); @@ -71,3 +74,47 @@ void channel_remove(jack_client_t *client, int idx) { atomic_store(&cur[idx].active, 0); atomic_fetch_sub(&channel_count, 1); } + +void channel_add_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + if (cur[idx].scene_count >= MAX_SCENES) + return; + int ns = cur[idx].scene_count; + init_scene(&cur[idx].scenes[ns]); + cur[idx].scene_count++; +} + +void channel_remove_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + if (cur[idx].scene_count <= 1) + return; + int cs = cur[idx].current_scene; + /* shift remaining scenes down */ + for (int i = cs; i < cur[idx].scene_count - 1; i++) { + cur[idx].scenes[i] = cur[idx].scenes[i + 1]; + } + cur[idx].scene_count--; + if (cur[idx].current_scene >= cur[idx].scene_count) + cur[idx].current_scene = cur[idx].scene_count - 1; +} + +void channel_next_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + if (cur[idx].scene_count > 1) { + cur[idx].current_scene = + (cur[idx].current_scene + 1) % cur[idx].scene_count; + } +} + +void channel_prev_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + if (cur[idx].scene_count > 1) { + cur[idx].current_scene = + (cur[idx].current_scene - 1 + cur[idx].scene_count) % + cur[idx].scene_count; + } +} diff --git a/src/channel.h b/src/channel.h index abe775b..0345482 100644 --- a/src/channel.h +++ b/src/channel.h @@ -9,6 +9,8 @@ #define MAX_MIDI_EVENTS 1024 +#define MAX_SCENES 16 + typedef enum { CHANNEL_AUDIO, CHANNEL_MIDI @@ -28,23 +30,28 @@ typedef enum { STATE_PAUSED } looper_state; -struct channel_t { - channel_type_t type; /* CHANNEL_AUDIO or CHANNEL_MIDI */ - - atomic_int state; - int prev_state; +typedef struct { union { float audio_buffer[LOOP_BUF_SIZE]; midi_event_t midi_events[MAX_MIDI_EVENTS]; } loop; - int loop_count; /* for audio: length in samples; for MIDI: number of recorded events */ - int record_pos; /* for audio: sample index; for MIDI: next event index for recording */ - int playback_pos; /* for audio: sample index; for MIDI: next event index for playback */ + int loop_count; + int record_pos; + int playback_pos; + atomic_int state; + int prev_state; +} scene_t; + +struct channel_t { + channel_type_t type; atomic_int active; jack_port_t *audio_in; jack_port_t *audio_out; jack_port_t *midi_in; jack_port_t *midi_out; + scene_t scenes[MAX_SCENES]; + int scene_count; + int current_scene; }; /* Globals declared in looper.c */ @@ -62,4 +69,10 @@ void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); void channel_add_midi(jack_client_t *client, int idx); +/* Scene management (called from main loop) */ +void channel_add_scene(jack_client_t *client, int idx); +void channel_remove_scene(jack_client_t *client, int idx); +void channel_next_scene(jack_client_t *client, int idx); +void channel_prev_scene(jack_client_t *client, int idx); + #endif diff --git a/src/command.h b/src/command.h index e6475d9..82eae59 100644 --- a/src/command.h +++ b/src/command.h @@ -2,13 +2,17 @@ #define COMMAND_H typedef enum { - CMD_CYCLE, // toggle record/stop for a channel - CMD_STOP, // force to idle + CMD_CYCLE, // toggle record/stop for the current scene of a channel + CMD_STOP, // force to idle for all scenes CMD_BIND_CHANNEL, // bind a channel index (data = channel) CMD_UNBIND, // reset bind to channel 0 CMD_ADD_CHANNEL, // add a new dynamic channel CMD_REMOVE_CHANNEL, // remove last dynamic channel CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel + CMD_NEXT_SCENE, + CMD_PREV_SCENE, + CMD_ADD_SCENE, + CMD_REMOVE_SCENE, } cmd_type_t; typedef struct { diff --git a/src/looper.c b/src/looper.c index d251ee6..0ac70ac 100644 --- a/src/looper.c +++ b/src/looper.c @@ -66,7 +66,9 @@ static void apply_command(command_t cmd) { switch (cmd.type) { case CMD_CYCLE: if (cmd.channel >= 0 && cmd.channel < cap) { - int cst = atomic_load(&cur[cmd.channel].state); + int sc_idx = cur[cmd.channel].current_scene; + scene_t *sc = &cur[cmd.channel].scenes[sc_idx]; + int cst = atomic_load(&sc->state); int next; switch (cst) { case STATE_IDLE: @@ -85,23 +87,29 @@ static void apply_command(command_t cmd) { next = STATE_IDLE; break; } - atomic_store(&cur[cmd.channel].state, next); + atomic_store(&sc->state, next); } break; case CMD_STOP: if (cmd.channel >= 0 && cmd.channel < cap) { - atomic_store(&cur[cmd.channel].state, STATE_IDLE); - cur[cmd.channel].loop_count = 0; - cur[cmd.channel].record_pos = 0; - cur[cmd.channel].playback_pos = 0; - cur[cmd.channel].prev_state = -1; + struct channel_t *ch = &cur[cmd.channel]; + for (int s = 0; s < ch->scene_count; s++) { + atomic_store(&ch->scenes[s].state, STATE_IDLE); + ch->scenes[s].loop_count = 0; + ch->scenes[s].record_pos = 0; + ch->scenes[s].playback_pos = 0; + ch->scenes[s].prev_state = -1; + } } else { for (int i = 0; i < cap; i++) { - atomic_store(&cur[i].state, STATE_IDLE); - cur[i].loop_count = 0; - cur[i].record_pos = 0; - cur[i].playback_pos = 0; - cur[i].prev_state = -1; + struct channel_t *ch = &cur[i]; + for (int s = 0; s < ch->scene_count; s++) { + atomic_store(&ch->scenes[s].state, STATE_IDLE); + ch->scenes[s].loop_count = 0; + ch->scenes[s].record_pos = 0; + ch->scenes[s].playback_pos = 0; + ch->scenes[s].prev_state = -1; + } } } break; @@ -158,6 +166,10 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } + /* Obtain current scene pointer */ + int sc_idx = active_channels[c].current_scene; + scene_t *sc = &active_channels[c].scenes[sc_idx]; + const jack_default_audio_sample_t *in = (const jack_default_audio_sample_t *)jack_port_get_buffer( active_channels[c].audio_in, nframes); @@ -167,18 +179,18 @@ int process_callback(jack_nframes_t nframes, void *arg) { if (!out) continue; - int state = atomic_load(&active_channels[c].state); + int state = atomic_load(&sc->state); - if (state != active_channels[c].prev_state) { + if (state != sc->prev_state) { switch (state) { case STATE_RECORD: - active_channels[c].record_pos = 0; - active_channels[c].loop_count = 0; + sc->record_pos = 0; + sc->loop_count = 0; break; case STATE_LOOPING: - if (active_channels[c].record_pos > 0) - active_channels[c].loop_count = active_channels[c].record_pos; - active_channels[c].playback_pos = 0; + if (sc->record_pos > 0) + sc->loop_count = sc->record_pos; + sc->playback_pos = 0; break; default: break; @@ -197,20 +209,14 @@ int process_callback(jack_nframes_t nframes, void *arg) { for (jack_nframes_t j = 0; j < nevents; j++) { if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) continue; - if (active_channels[c].record_pos < MAX_MIDI_EVENTS) { - active_channels[c] - .loop.midi_events[active_channels[c].record_pos] - .timestamp = ev.time; - active_channels[c] - .loop.midi_events[active_channels[c].record_pos] - .status = ev.buffer[0]; - active_channels[c] - .loop.midi_events[active_channels[c].record_pos] - .note = (ev.size > 1) ? ev.buffer[1] : 0; - active_channels[c] - .loop.midi_events[active_channels[c].record_pos] - .velocity = (ev.size > 2) ? ev.buffer[2] : 0; - active_channels[c].record_pos++; + if (sc->record_pos < MAX_MIDI_EVENTS) { + sc->loop.midi_events[sc->record_pos].timestamp = ev.time; + sc->loop.midi_events[sc->record_pos].status = ev.buffer[0]; + sc->loop.midi_events[sc->record_pos].note = + (ev.size > 1) ? ev.buffer[1] : 0; + sc->loop.midi_events[sc->record_pos].velocity = + (ev.size > 2) ? ev.buffer[2] : 0; + sc->record_pos++; } } /* forward incoming MIDI to output during record */ @@ -232,15 +238,13 @@ int process_callback(jack_nframes_t nframes, void *arg) { jack_port_get_buffer(active_channels[c].midi_out, nframes); if (midi_out_buf) { jack_midi_clear_buffer(midi_out_buf); - int cnt = - active_channels[c].loop_count; /* number of recorded events */ + int cnt = sc->loop_count; if (cnt > 0) { - /* simple: output all recorded events at frame 0 of each cycle */ for (int e = 0; e < cnt; e++) { unsigned char msg[3]; - msg[0] = active_channels[c].loop.midi_events[e].status; - msg[1] = active_channels[c].loop.midi_events[e].note; - msg[2] = active_channels[c].loop.midi_events[e].velocity; + msg[0] = sc->loop.midi_events[e].status; + msg[1] = sc->loop.midi_events[e].note; + msg[2] = sc->loop.midi_events[e].velocity; jack_midi_event_write(midi_out_buf, 0, msg, 3); } } @@ -251,7 +255,6 @@ int process_callback(jack_nframes_t nframes, void *arg) { /* no output */ break; default: /* IDLE */ - /* pass through MIDI input to output */ { void *midi_in_buf = jack_port_get_buffer(active_channels[c].midi_in, nframes); @@ -270,9 +273,8 @@ int process_callback(jack_nframes_t nframes, void *arg) { } break; } - /* for MIDI channels, the loop_count holds number of recorded events */ if (state == STATE_LOOPING) { - active_channels[c].loop_count = active_channels[c].record_pos; + sc->loop_count = sc->record_pos; } } else { /* audio channel handling */ @@ -283,9 +285,8 @@ int process_callback(jack_nframes_t nframes, void *arg) { float *f_out = (float *)out; const float *f_in = (const float *)in; for (i = 0; i < nframes; i++) { - if (active_channels[c].record_pos < LOOP_BUF_SIZE) - active_channels[c] - .loop.audio_buffer[active_channels[c].record_pos++] = f_in[i]; + if (sc->record_pos < LOOP_BUF_SIZE) + sc->loop.audio_buffer[sc->record_pos++] = f_in[i]; f_out[i] = f_in[i]; } } else { @@ -294,14 +295,11 @@ int process_callback(jack_nframes_t nframes, void *arg) { break; case STATE_LOOPING: - if (active_channels[c].loop_count > 0) { + if (sc->loop_count > 0) { float *outf = (float *)out; for (i = 0; i < nframes; i++) { - outf[i] = active_channels[c] - .loop.audio_buffer[active_channels[c].playback_pos]; - active_channels[c].playback_pos = - (active_channels[c].playback_pos + 1) % - active_channels[c].loop_count; + outf[i] = sc->loop.audio_buffer[sc->playback_pos]; + sc->playback_pos = (sc->playback_pos + 1) % sc->loop_count; } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); @@ -322,7 +320,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } - active_channels[c].prev_state = state; + sc->prev_state = state; } /* MIDI clock events – affect channel 0 only */ @@ -392,12 +390,14 @@ int looper_init(jack_client_t *client) { } struct channel_t *init = atomic_load(&channels); /* channel 0 */ - init[0].active = 1; - atomic_store(&init[0].state, STATE_IDLE); - init[0].prev_state = -1; - init[0].loop_count = 0; - init[0].record_pos = 0; - init[0].playback_pos = 0; + atomic_store(&init[0].active, 1); + init[0].scene_count = 1; + init[0].current_scene = 0; + init[0].scenes[0].loop_count = 0; + init[0].scenes[0].record_pos = 0; + init[0].scenes[0].playback_pos = 0; + atomic_store(&init[0].scenes[0].state, STATE_IDLE); + init[0].scenes[0].prev_state = -1; init[0].audio_in = jack_port_register( client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); @@ -471,6 +471,46 @@ void looper_process_commands(jack_client_t *client) { } break; } + case CMD_ADD_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_add_scene(client, ch); + } + break; + } + case CMD_REMOVE_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_remove_scene(client, ch); + } + break; + } + case CMD_NEXT_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_next_scene(client, ch); + } + break; + } + case CMD_PREV_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_prev_scene(client, ch); + } + break; + } default: break; } @@ -519,6 +559,46 @@ void looper_process_commands(jack_client_t *client) { } break; } + case CMD_ADD_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_add_scene(client, ch); + } + break; + } + case CMD_REMOVE_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_remove_scene(client, ch); + } + break; + } + case CMD_NEXT_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_next_scene(client, ch); + } + break; + } + case CMD_PREV_SCENE: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_prev_scene(client, ch); + } + break; + } default: break; } diff --git a/src/midi.c b/src/midi.c index 9bca166..410c990 100644 --- a/src/midi.c +++ b/src/midi.c @@ -71,6 +71,26 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { .type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; + case 67: { + command_t cmd = { + .type = CMD_NEXT_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 68: { + command_t cmd = { + .type = CMD_PREV_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 69: { + command_t cmd = { + .type = CMD_ADD_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 70: { + command_t cmd = { + .type = CMD_REMOVE_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; default: break; } diff --git a/src/pipe.c b/src/pipe.c index 1cb6700..ae6d553 100644 --- a/src/pipe.c +++ b/src/pipe.c @@ -54,6 +54,18 @@ static void *pipe_thread_func(void *arg) { } else if (strcmp(line, "unbind") == 0) { command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0}; queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "scene_add") == 0) { + command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0}; + 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_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_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_main_fifo, cmd); } /* ignore unknown lines */ }