From 5739ff801932e450bee4c2a4864e78fc337380a1 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 10:38:59 +0000 Subject: [PATCH] feat: remove hard limit on number of channels Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 30 +++++---- src/channel.h | 9 ++- src/looper.c | 176 +++++++++++++++++++++++++++++++++----------------- src/midi.c | 4 +- 4 files changed, 140 insertions(+), 79 deletions(-) diff --git a/src/channel.c b/src/channel.c index 8eaf4d7..542d63a 100644 --- a/src/channel.c +++ b/src/channel.c @@ -6,35 +6,37 @@ #include void channel_add(jack_client_t *client, int idx) { + struct channel_t *cur = get_channels_array(); + 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); - channels[idx].audio_in = jack_port_register( + cur[idx].audio_in = jack_port_register( client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - channels[idx].audio_out = jack_port_register( + cur[idx].audio_out = jack_port_register( client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - if (!channels[idx].audio_in || !channels[idx].audio_out) { + if (!cur[idx].audio_in || !cur[idx].audio_out) { fprintf(stderr, "Failed to register ports for channel %d\n", next_channel_id); - /* Do NOT mark channel active – process loop will skip it */ - atomic_store(&channels[idx].active, 0); + atomic_store(&cur[idx].active, 0); return; } - atomic_store(&channels[idx].active, 1); - atomic_store(&channels[idx].state, STATE_IDLE); - channels[idx].prev_state = -1; - channels[idx].loop_count = 0; - channels[idx].record_pos = 0; - channels[idx].playback_pos = 0; + 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; next_channel_id++; - channel_count++; + atomic_fetch_add(&channel_count, 1); } void channel_remove(jack_client_t *client, int idx) { (void)client; - atomic_store(&channels[idx].active, 0); - channel_count--; + struct channel_t *cur = get_channels_array(); + atomic_store(&cur[idx].active, 0); + atomic_fetch_sub(&channel_count, 1); } diff --git a/src/channel.h b/src/channel.h index 64a0ece..0d293ed 100644 --- a/src/channel.h +++ b/src/channel.h @@ -6,7 +6,6 @@ #include #define LOOP_BUF_SIZE (5 * 48000) -#define MAX_CHANNELS 16 typedef enum { STATE_IDLE, @@ -28,10 +27,16 @@ struct channel_t { }; /* Globals declared in looper.c */ -extern struct channel_t channels[MAX_CHANNELS]; +extern _Atomic struct channel_t *channels; +extern atomic_int channel_capacity; extern atomic_int channel_count; extern int next_channel_id; +/* Safe accessor for the real‑time thread (returns a snapshot of the current pointer) */ +static inline struct channel_t *get_channels_array(void) { + return atomic_load(&channels); +} + void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); diff --git a/src/looper.c b/src/looper.c index 6f6e2d5..10e39b2 100644 --- a/src/looper.c +++ b/src/looper.c @@ -13,7 +13,8 @@ #include /* Global state (shared across files) */ -struct channel_t channels[MAX_CHANNELS]; +_Atomic struct channel_t *channels = NULL; +atomic_int channel_capacity = 0; atomic_int channel_count = 0; int next_channel_id = 1; spsc_queue_t cmd_queue_main_midi; @@ -29,13 +30,38 @@ spsc_queue_t cmd_queue; static int pending_unregister_idx = -1; static int pending_unregister_cycle = 0; +/* Helper: grow the channel array so that index idx is valid */ +static int ensure_capacity(jack_client_t *client, int idx) { + (void)client; + int cur_cap = atomic_load(&channel_capacity); + if (idx < cur_cap) + return 0; + int new_cap = cur_cap == 0 ? 8 : cur_cap; + while (new_cap <= idx) + new_cap *= 2; + struct channel_t *new_arr = calloc(new_cap, sizeof(struct channel_t)); + if (!new_arr) + return -1; + /* copy existing channels */ + if (cur_cap > 0) + memcpy(new_arr, atomic_load(&channels), cur_cap * sizeof(struct channel_t)); + /* atomically publish new array */ + struct channel_t *old = atomic_exchange(&channels, new_arr); + atomic_store(&channel_capacity, new_cap); + free(old); + return 0; +} + static void apply_command(command_t cmd) { + struct channel_t *cur = get_channels_array(); + int cap = atomic_load(&channel_capacity); + switch (cmd.type) { case CMD_CYCLE: - if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) { - int cur = atomic_load(&channels[cmd.channel].state); + if (cmd.channel >= 0 && cmd.channel < cap) { + int cst = atomic_load(&cur[cmd.channel].state); int next; - switch (cur) { + switch (cst) { case STATE_IDLE: next = STATE_RECORD; break; @@ -52,15 +78,15 @@ static void apply_command(command_t cmd) { next = STATE_IDLE; break; } - atomic_store(&channels[cmd.channel].state, next); + atomic_store(&cur[cmd.channel].state, next); } break; case CMD_STOP: - if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) - atomic_store(&channels[cmd.channel].state, STATE_IDLE); + if (cmd.channel >= 0 && cmd.channel < cap) + atomic_store(&cur[cmd.channel].state, STATE_IDLE); else { - for (int i = 0; i < MAX_CHANNELS; i++) - atomic_store(&channels[i].state, STATE_IDLE); + for (int i = 0; i < cap; i++) + atomic_store(&cur[i].state, STATE_IDLE); } break; case CMD_BIND_CHANNEL: @@ -94,37 +120,39 @@ int process_callback(jack_nframes_t nframes, void *arg) { } /* process each active channel */ - for (int c = 0; c < MAX_CHANNELS; c++) { - if (!atomic_load(&channels[c].active)) + struct channel_t *active_channels = get_channels_array(); + int cap = atomic_load(&channel_capacity); + for (int c = 0; c < cap; c++) { + if (!atomic_load(&active_channels[c].active)) continue; /* Guard against NULL ports (e.g. if port registration failed) */ - if (!channels[c].audio_in || !channels[c].audio_out) { + if (!active_channels[c].audio_in || !active_channels[c].audio_out) { fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c); continue; } const jack_default_audio_sample_t *in = (const jack_default_audio_sample_t *)jack_port_get_buffer( - channels[c].audio_in, nframes); + active_channels[c].audio_in, nframes); jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)jack_port_get_buffer( - channels[c].audio_out, nframes); + active_channels[c].audio_out, nframes); if (!out) continue; - int state = atomic_load(&channels[c].state); + int state = atomic_load(&active_channels[c].state); - if (state != channels[c].prev_state) { + if (state != active_channels[c].prev_state) { switch (state) { case STATE_RECORD: - channels[c].record_pos = 0; - channels[c].loop_count = 0; + active_channels[c].record_pos = 0; + active_channels[c].loop_count = 0; break; case STATE_LOOPING: - if (channels[c].record_pos > 0) - channels[c].loop_count = channels[c].record_pos; - channels[c].playback_pos = 0; + if (active_channels[c].record_pos > 0) + active_channels[c].loop_count = active_channels[c].record_pos; + active_channels[c].playback_pos = 0; break; default: break; @@ -138,8 +166,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 (channels[c].record_pos < LOOP_BUF_SIZE) - channels[c].loop_buffer[channels[c].record_pos++] = f_in[i]; + if (active_channels[c].record_pos < LOOP_BUF_SIZE) + active_channels[c].loop_buffer[active_channels[c].record_pos++] = f_in[i]; f_out[i] = f_in[i]; } } else { @@ -148,12 +176,12 @@ int process_callback(jack_nframes_t nframes, void *arg) { break; case STATE_LOOPING: - if (channels[c].loop_count > 0) { + if (active_channels[c].loop_count > 0) { float *outf = (float *)out; for (i = 0; i < nframes; i++) { - outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; - channels[c].playback_pos = - (channels[c].playback_pos + 1) % channels[c].loop_count; + outf[i] = active_channels[c].loop_buffer[active_channels[c].playback_pos]; + active_channels[c].playback_pos = + (active_channels[c].playback_pos + 1) % active_channels[c].loop_count; } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); @@ -173,7 +201,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { break; } - channels[c].prev_state = state; + active_channels[c].prev_state = state; } /* MIDI clock events – affect channel 0 only */ @@ -189,18 +217,22 @@ int process_callback(jack_nframes_t nframes, void *arg) { unsigned char msg = cev.buffer[0]; switch (msg) { case 0xFA: { - int s = atomic_load(&channels[0].state); + struct channel_t *cur = atomic_load(&channels); + int s = atomic_load(&cur[0].state); if (s == STATE_IDLE) - atomic_store(&channels[0].state, STATE_RECORD); + atomic_store(&cur[0].state, STATE_RECORD); break; } - case 0xFC: - atomic_store(&channels[0].state, STATE_IDLE); + case 0xFC: { + struct channel_t *cur = atomic_load(&channels); + atomic_store(&cur[0].state, STATE_IDLE); break; + } case 0xFB: { - int s = atomic_load(&channels[0].state); + struct channel_t *cur = atomic_load(&channels); + int s = atomic_load(&cur[0].state); if (s == STATE_PAUSED) - atomic_store(&channels[0].state, STATE_LOOPING); + atomic_store(&cur[0].state, STATE_LOOPING); break; } default: @@ -231,23 +263,30 @@ int looper_init(jack_client_t *client) { queue_init(&cmd_queue); queue_init(&cmd_queue_main_midi); queue_init(&cmd_queue_main_fifo); - /* channel 0 */ - channels[0].active = 1; - atomic_store(&channels[0].state, STATE_IDLE); - channels[0].prev_state = -1; - channels[0].loop_count = 0; - channels[0].record_pos = 0; - channels[0].playback_pos = 0; - channels[0].audio_in = jack_port_register( + /* allocate initial array for at least one channel */ + if (ensure_capacity(client, 0) != 0) { + fprintf(stderr, "Cannot allocate channel array\n"); + return -1; + } + 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; + + init[0].audio_in = jack_port_register( client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - channels[0].audio_out = jack_port_register( + init[0].audio_out = jack_port_register( client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); - if (!channels[0].audio_in || !channels[0].audio_out) { + if (!init[0].audio_in || !init[0].audio_out) { fprintf(stderr, "Could not create audio ports for channel 0\n"); return -1; } - channel_count = 1; + atomic_store(&channel_count, 1); midi_control_port = jack_port_register( client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); @@ -270,18 +309,25 @@ void looper_process_commands(jack_client_t *client) { while (queue_pop(&cmd_queue_main_midi, &cmd)) { switch (cmd.type) { case CMD_ADD_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); int idx; - for (idx = 0; idx < MAX_CHANNELS; idx++) - if (!channels[idx].active) + for (idx = 0; idx < cap; idx++) + if (!atomic_load(&cur[idx].active)) break; - if (idx < MAX_CHANNELS) - channel_add(client, idx); + if (idx == cap) { + if (ensure_capacity(client, idx) != 0) + break; + } + channel_add(client, idx); break; } case CMD_REMOVE_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); int remove_idx = -1; - for (int idx = 1; idx < MAX_CHANNELS; idx++) - if (channels[idx].active) + for (int idx = 1; idx < cap; idx++) + if (atomic_load(&cur[idx].active)) remove_idx = idx; if (remove_idx != -1) { channel_remove(client, remove_idx); @@ -297,18 +343,25 @@ void looper_process_commands(jack_client_t *client) { while (queue_pop(&cmd_queue_main_fifo, &cmd)) { switch (cmd.type) { case CMD_ADD_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); int idx; - for (idx = 0; idx < MAX_CHANNELS; idx++) - if (!channels[idx].active) + for (idx = 0; idx < cap; idx++) + if (!atomic_load(&cur[idx].active)) break; - if (idx < MAX_CHANNELS) - channel_add(client, idx); + if (idx == cap) { + if (ensure_capacity(client, idx) != 0) + break; + } + channel_add(client, idx); break; } case CMD_REMOVE_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); int remove_idx = -1; - for (int idx = 1; idx < MAX_CHANNELS; idx++) - if (channels[idx].active) + for (int idx = 1; idx < cap; idx++) + if (atomic_load(&cur[idx].active)) remove_idx = idx; if (remove_idx != -1) { channel_remove(client, remove_idx); @@ -327,10 +380,11 @@ void looper_process_commands(jack_client_t *client) { int current_cycle = atomic_load(&global_rt_cycles); if (current_cycle - pending_unregister_cycle >= 1) { int idx = pending_unregister_idx; - if (channels[idx].audio_in) - jack_port_unregister(client, channels[idx].audio_in); - if (channels[idx].audio_out) - jack_port_unregister(client, channels[idx].audio_out); + struct channel_t *cur = atomic_load(&channels); + if (cur[idx].audio_in) + jack_port_unregister(client, cur[idx].audio_in); + if (cur[idx].audio_out) + jack_port_unregister(client, cur[idx].audio_out); pending_unregister_idx = -1; } } diff --git a/src/midi.c b/src/midi.c index 6cfddaf..0cb8e9d 100644 --- a/src/midi.c +++ b/src/midi.c @@ -35,7 +35,7 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { int ck = atomic_load(&control_key_active); if (ck) { atomic_store(&control_key_active, 0); - if (note < 16) { + if (note < 16 && note < atomic_load(&channel_capacity)) { command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = note}; queue_push(&cmd_queue, cmd); @@ -53,7 +53,7 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { } break; case 62: { int bch = atomic_load(&bind_channel); - if (bch >= 0 && bch < MAX_CHANNELS) { + if (bch >= 0 && bch < atomic_load(&channel_capacity)) { command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0}; queue_push(&cmd_queue, cmd); }