feat: remove hard limit on number of channels

Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-10 10:38:59 +00:00
parent 3a4aac3356
commit 5739ff8019
4 changed files with 140 additions and 79 deletions

View File

@@ -13,7 +13,8 @@
#include <string.h>
/* 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;
}
}