2-midi-looping #3

Merged
boomjacky merged 17 commits from 2-midi-looping into master 2026-05-10 12:24:23 -04:00
4 changed files with 140 additions and 79 deletions
Showing only changes of commit 5739ff8019 - Show all commits

View File

@@ -6,35 +6,37 @@
#include <string.h> #include <string.h>
void channel_add(jack_client_t *client, int idx) { void channel_add(jack_client_t *client, int idx) {
struct channel_t *cur = get_channels_array();
char in_name[64], out_name[64]; char in_name[64], out_name[64];
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id); snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
snprintf(out_name, sizeof(out_name), "channel%d_output", 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); 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); 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", fprintf(stderr, "Failed to register ports for channel %d\n",
next_channel_id); next_channel_id);
/* Do NOT mark channel active process loop will skip it */ atomic_store(&cur[idx].active, 0);
atomic_store(&channels[idx].active, 0);
return; return;
} }
atomic_store(&channels[idx].active, 1); atomic_store(&cur[idx].active, 1);
atomic_store(&channels[idx].state, STATE_IDLE); atomic_store(&cur[idx].state, STATE_IDLE);
channels[idx].prev_state = -1; cur[idx].prev_state = -1;
channels[idx].loop_count = 0; cur[idx].loop_count = 0;
channels[idx].record_pos = 0; cur[idx].record_pos = 0;
channels[idx].playback_pos = 0; cur[idx].playback_pos = 0;
next_channel_id++; next_channel_id++;
channel_count++; atomic_fetch_add(&channel_count, 1);
} }
void channel_remove(jack_client_t *client, int idx) { void channel_remove(jack_client_t *client, int idx) {
(void)client; (void)client;
atomic_store(&channels[idx].active, 0); struct channel_t *cur = get_channels_array();
channel_count--; atomic_store(&cur[idx].active, 0);
atomic_fetch_sub(&channel_count, 1);
} }

View File

@@ -6,7 +6,6 @@
#include <stdatomic.h> #include <stdatomic.h>
#define LOOP_BUF_SIZE (5 * 48000) #define LOOP_BUF_SIZE (5 * 48000)
#define MAX_CHANNELS 16
typedef enum { typedef enum {
STATE_IDLE, STATE_IDLE,
@@ -28,10 +27,16 @@ struct channel_t {
}; };
/* Globals declared in looper.c */ /* 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 atomic_int channel_count;
extern int next_channel_id; extern int next_channel_id;
/* Safe accessor for the realtime 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_add(jack_client_t *client, int idx);
void channel_remove(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx);

View File

@@ -13,7 +13,8 @@
#include <string.h> #include <string.h>
/* Global state (shared across files) */ /* 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; atomic_int channel_count = 0;
int next_channel_id = 1; int next_channel_id = 1;
spsc_queue_t cmd_queue_main_midi; 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_idx = -1;
static int pending_unregister_cycle = 0; 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) { static void apply_command(command_t cmd) {
struct channel_t *cur = get_channels_array();
int cap = atomic_load(&channel_capacity);
switch (cmd.type) { switch (cmd.type) {
case CMD_CYCLE: case CMD_CYCLE:
if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) { if (cmd.channel >= 0 && cmd.channel < cap) {
int cur = atomic_load(&channels[cmd.channel].state); int cst = atomic_load(&cur[cmd.channel].state);
int next; int next;
switch (cur) { switch (cst) {
case STATE_IDLE: case STATE_IDLE:
next = STATE_RECORD; next = STATE_RECORD;
break; break;
@@ -52,15 +78,15 @@ static void apply_command(command_t cmd) {
next = STATE_IDLE; next = STATE_IDLE;
break; break;
} }
atomic_store(&channels[cmd.channel].state, next); atomic_store(&cur[cmd.channel].state, next);
} }
break; break;
case CMD_STOP: case CMD_STOP:
if (cmd.channel >= 0 && cmd.channel < MAX_CHANNELS) if (cmd.channel >= 0 && cmd.channel < cap)
atomic_store(&channels[cmd.channel].state, STATE_IDLE); atomic_store(&cur[cmd.channel].state, STATE_IDLE);
else { else {
for (int i = 0; i < MAX_CHANNELS; i++) for (int i = 0; i < cap; i++)
atomic_store(&channels[i].state, STATE_IDLE); atomic_store(&cur[i].state, STATE_IDLE);
} }
break; break;
case CMD_BIND_CHANNEL: case CMD_BIND_CHANNEL:
@@ -94,37 +120,39 @@ int process_callback(jack_nframes_t nframes, void *arg) {
} }
/* process each active channel */ /* process each active channel */
for (int c = 0; c < MAX_CHANNELS; c++) { struct channel_t *active_channels = get_channels_array();
if (!atomic_load(&channels[c].active)) int cap = atomic_load(&channel_capacity);
for (int c = 0; c < cap; c++) {
if (!atomic_load(&active_channels[c].active))
continue; continue;
/* Guard against NULL ports (e.g. if port registration failed) */ /* 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); fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c);
continue; continue;
} }
const jack_default_audio_sample_t *in = const jack_default_audio_sample_t *in =
(const jack_default_audio_sample_t *)jack_port_get_buffer( (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 *out =
(jack_default_audio_sample_t *)jack_port_get_buffer( (jack_default_audio_sample_t *)jack_port_get_buffer(
channels[c].audio_out, nframes); active_channels[c].audio_out, nframes);
if (!out) if (!out)
continue; 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) { switch (state) {
case STATE_RECORD: case STATE_RECORD:
channels[c].record_pos = 0; active_channels[c].record_pos = 0;
channels[c].loop_count = 0; active_channels[c].loop_count = 0;
break; break;
case STATE_LOOPING: case STATE_LOOPING:
if (channels[c].record_pos > 0) if (active_channels[c].record_pos > 0)
channels[c].loop_count = channels[c].record_pos; active_channels[c].loop_count = active_channels[c].record_pos;
channels[c].playback_pos = 0; active_channels[c].playback_pos = 0;
break; break;
default: default:
break; break;
@@ -138,8 +166,8 @@ int process_callback(jack_nframes_t nframes, void *arg) {
float *f_out = (float *)out; float *f_out = (float *)out;
const float *f_in = (const float *)in; const float *f_in = (const float *)in;
for (i = 0; i < nframes; i++) { for (i = 0; i < nframes; i++) {
if (channels[c].record_pos < LOOP_BUF_SIZE) if (active_channels[c].record_pos < LOOP_BUF_SIZE)
channels[c].loop_buffer[channels[c].record_pos++] = f_in[i]; active_channels[c].loop_buffer[active_channels[c].record_pos++] = f_in[i];
f_out[i] = f_in[i]; f_out[i] = f_in[i];
} }
} else { } else {
@@ -148,12 +176,12 @@ int process_callback(jack_nframes_t nframes, void *arg) {
break; break;
case STATE_LOOPING: case STATE_LOOPING:
if (channels[c].loop_count > 0) { if (active_channels[c].loop_count > 0) {
float *outf = (float *)out; float *outf = (float *)out;
for (i = 0; i < nframes; i++) { for (i = 0; i < nframes; i++) {
outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; outf[i] = active_channels[c].loop_buffer[active_channels[c].playback_pos];
channels[c].playback_pos = active_channels[c].playback_pos =
(channels[c].playback_pos + 1) % channels[c].loop_count; (active_channels[c].playback_pos + 1) % active_channels[c].loop_count;
} }
} else { } else {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
@@ -173,7 +201,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
break; break;
} }
channels[c].prev_state = state; active_channels[c].prev_state = state;
} }
/* MIDI clock events affect channel 0 only */ /* 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]; unsigned char msg = cev.buffer[0];
switch (msg) { switch (msg) {
case 0xFA: { 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) if (s == STATE_IDLE)
atomic_store(&channels[0].state, STATE_RECORD); atomic_store(&cur[0].state, STATE_RECORD);
break; break;
} }
case 0xFC: case 0xFC: {
atomic_store(&channels[0].state, STATE_IDLE); struct channel_t *cur = atomic_load(&channels);
atomic_store(&cur[0].state, STATE_IDLE);
break; break;
}
case 0xFB: { 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) if (s == STATE_PAUSED)
atomic_store(&channels[0].state, STATE_LOOPING); atomic_store(&cur[0].state, STATE_LOOPING);
break; break;
} }
default: default:
@@ -231,23 +263,30 @@ int looper_init(jack_client_t *client) {
queue_init(&cmd_queue); queue_init(&cmd_queue);
queue_init(&cmd_queue_main_midi); queue_init(&cmd_queue_main_midi);
queue_init(&cmd_queue_main_fifo); 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); 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); 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"); fprintf(stderr, "Could not create audio ports for channel 0\n");
return -1; return -1;
} }
channel_count = 1; atomic_store(&channel_count, 1);
midi_control_port = jack_port_register( midi_control_port = jack_port_register(
client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); 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)) { while (queue_pop(&cmd_queue_main_midi, &cmd)) {
switch (cmd.type) { switch (cmd.type) {
case CMD_ADD_CHANNEL: { case CMD_ADD_CHANNEL: {
int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array();
int idx; int idx;
for (idx = 0; idx < MAX_CHANNELS; idx++) for (idx = 0; idx < cap; idx++)
if (!channels[idx].active) if (!atomic_load(&cur[idx].active))
break; break;
if (idx < MAX_CHANNELS) if (idx == cap) {
channel_add(client, idx); if (ensure_capacity(client, idx) != 0)
break;
}
channel_add(client, idx);
break; break;
} }
case CMD_REMOVE_CHANNEL: { case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array();
int remove_idx = -1; int remove_idx = -1;
for (int idx = 1; idx < MAX_CHANNELS; idx++) for (int idx = 1; idx < cap; idx++)
if (channels[idx].active) if (atomic_load(&cur[idx].active))
remove_idx = idx; remove_idx = idx;
if (remove_idx != -1) { if (remove_idx != -1) {
channel_remove(client, remove_idx); 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)) { while (queue_pop(&cmd_queue_main_fifo, &cmd)) {
switch (cmd.type) { switch (cmd.type) {
case CMD_ADD_CHANNEL: { case CMD_ADD_CHANNEL: {
int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array();
int idx; int idx;
for (idx = 0; idx < MAX_CHANNELS; idx++) for (idx = 0; idx < cap; idx++)
if (!channels[idx].active) if (!atomic_load(&cur[idx].active))
break; break;
if (idx < MAX_CHANNELS) if (idx == cap) {
channel_add(client, idx); if (ensure_capacity(client, idx) != 0)
break;
}
channel_add(client, idx);
break; break;
} }
case CMD_REMOVE_CHANNEL: { case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array();
int remove_idx = -1; int remove_idx = -1;
for (int idx = 1; idx < MAX_CHANNELS; idx++) for (int idx = 1; idx < cap; idx++)
if (channels[idx].active) if (atomic_load(&cur[idx].active))
remove_idx = idx; remove_idx = idx;
if (remove_idx != -1) { if (remove_idx != -1) {
channel_remove(client, remove_idx); 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); int current_cycle = atomic_load(&global_rt_cycles);
if (current_cycle - pending_unregister_cycle >= 1) { if (current_cycle - pending_unregister_cycle >= 1) {
int idx = pending_unregister_idx; int idx = pending_unregister_idx;
if (channels[idx].audio_in) struct channel_t *cur = atomic_load(&channels);
jack_port_unregister(client, channels[idx].audio_in); if (cur[idx].audio_in)
if (channels[idx].audio_out) jack_port_unregister(client, cur[idx].audio_in);
jack_port_unregister(client, channels[idx].audio_out); if (cur[idx].audio_out)
jack_port_unregister(client, cur[idx].audio_out);
pending_unregister_idx = -1; pending_unregister_idx = -1;
} }
} }

View File

@@ -35,7 +35,7 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
int ck = atomic_load(&control_key_active); int ck = atomic_load(&control_key_active);
if (ck) { if (ck) {
atomic_store(&control_key_active, 0); atomic_store(&control_key_active, 0);
if (note < 16) { if (note < 16 && note < atomic_load(&channel_capacity)) {
command_t cmd = { command_t cmd = {
.type = CMD_BIND_CHANNEL, .channel = -1, .data = note}; .type = CMD_BIND_CHANNEL, .channel = -1, .data = note};
queue_push(&cmd_queue, cmd); queue_push(&cmd_queue, cmd);
@@ -53,7 +53,7 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
} break; } break;
case 62: { case 62: {
int bch = atomic_load(&bind_channel); 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}; command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0};
queue_push(&cmd_queue, cmd); queue_push(&cmd_queue, cmd);
} }