Files
looper/src/looper.c
Loic Coenen 74db4ed46c fix: add missing channel pointer declaration in apply_command
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
2026-05-10 19:13:51 +00:00

624 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include "channel.h"
#include "command.h"
#include "midi.h"
#include "queue.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Global state (shared across files) */
struct channel_t *_Atomic channels = NULL;
atomic_int channel_capacity = 0;
atomic_int channel_count = 0;
int next_channel_id = 1;
spsc_queue_t cmd_queue_main_midi;
spsc_queue_t cmd_queue_main_fifo;
atomic_int global_rt_cycles = 0;
jack_port_t *midi_control_port = NULL;
jack_port_t *midi_clock_port = NULL;
atomic_int control_key_active = 0;
atomic_int bind_channel = 0;
spsc_queue_t cmd_queue;
/* Deferred removal index and cycle counter */
static int pending_unregister_idx = -1;
static int pending_unregister_cycle = 0;
/* Deferred free of old channel array (must not free while RT thread may hold
* pointer) */
static struct channel_t *pending_old = NULL;
static int pending_old_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, defer free of old */
struct channel_t *old = atomic_exchange(&channels, new_arr);
atomic_store(&channel_capacity, new_cap);
/* schedule old pointer for later deallocation (after RT cycle) */
pending_old = old;
pending_old_cycle = atomic_load(&global_rt_cycles);
return 0;
}
static void apply_command(command_t cmd) {
int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array();
switch (cmd.type) {
case CMD_CYCLE:
if (cmd.channel >= 0 && cmd.channel < cap) {
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:
next = STATE_RECORD;
break;
case STATE_RECORD:
next = STATE_LOOPING;
break;
case STATE_LOOPING:
next = STATE_PAUSED;
break;
case STATE_PAUSED:
next = STATE_LOOPING;
break;
default:
next = STATE_IDLE;
break;
}
atomic_store(&sc->state, next);
}
break;
case CMD_STOP:
if (cmd.channel >= 0 && cmd.channel < cap) {
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++) {
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;
case CMD_BIND_CHANNEL:
atomic_store(&bind_channel, cmd.data);
break;
case CMD_UNBIND:
atomic_store(&bind_channel, 0);
break;
default:
break;
}
}
/* ----------------------------------------------------------------
* process callback
* ---------------------------------------------------------------- */
int process_callback(jack_nframes_t nframes, void *arg) {
(void)arg;
if (midi_control_port) {
void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes);
if (midi_ctrl_buf) {
midi_handle_events(midi_ctrl_buf, nframes);
}
}
/* drain RTsafe commands */
command_t cmd;
while (queue_pop(&cmd_queue, &cmd)) {
apply_command(cmd);
}
/* process each active channel */
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 (active_channels[c].type == CHANNEL_AUDIO) {
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;
}
} else {
/* CHANNEL_MIDI */
if (!active_channels[c].midi_in || !active_channels[c].midi_out) {
fprintf(stderr, "WARN: channel %d has NULL MIDI port(s), skipping\n",
c);
continue;
}
}
/* 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);
jack_default_audio_sample_t *out =
(jack_default_audio_sample_t *)jack_port_get_buffer(
active_channels[c].audio_out, nframes);
if (!out)
continue;
int state = atomic_load(&sc->state);
if (state != sc->prev_state) {
switch (state) {
case STATE_RECORD:
sc->record_pos = 0;
sc->loop_count = 0;
break;
case STATE_LOOPING:
if (sc->record_pos > 0)
sc->loop_count = sc->record_pos;
sc->playback_pos = 0;
break;
default:
break;
}
}
if (active_channels[c].type == CHANNEL_MIDI) {
/* MIDI channel handling */
switch (state) {
case STATE_RECORD: {
void *midi_in_buf =
jack_port_get_buffer(active_channels[c].midi_in, nframes);
if (midi_in_buf) {
jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf);
jack_midi_event_t ev;
for (jack_nframes_t j = 0; j < nevents; j++) {
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
continue;
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 */
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_out_buf) {
jack_midi_clear_buffer(midi_out_buf);
for (jack_nframes_t j = 0; j < nevents; j++) {
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
continue;
jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size);
}
}
}
break;
}
case STATE_LOOPING: {
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_out_buf) {
jack_midi_clear_buffer(midi_out_buf);
int cnt = sc->loop_count;
if (cnt > 0) {
for (int e = 0; e < cnt; e++) {
unsigned char msg[3];
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);
}
}
}
break;
}
case STATE_PAUSED:
/* no output */
break;
default: /* IDLE */
{
void *midi_in_buf =
jack_port_get_buffer(active_channels[c].midi_in, nframes);
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_in_buf && midi_out_buf) {
jack_midi_clear_buffer(midi_out_buf);
jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf);
jack_midi_event_t ev;
for (jack_nframes_t j = 0; j < nevents; j++) {
if (jack_midi_event_get(&ev, midi_in_buf, j) != 0)
continue;
jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size);
}
}
}
break;
}
if (state == STATE_LOOPING) {
sc->loop_count = sc->record_pos;
}
} else {
/* audio channel handling */
jack_nframes_t i;
switch (state) {
case STATE_RECORD:
if (in) {
float *f_out = (float *)out;
const float *f_in = (const float *)in;
for (i = 0; i < nframes; 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 {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
}
break;
case STATE_LOOPING:
if (sc->loop_count > 0) {
float *outf = (float *)out;
for (i = 0; i < nframes; i++) {
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);
}
break;
case STATE_PAUSED:
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
break;
default: /* IDLE */
if (in) {
memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes);
} else {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
}
break;
}
}
sc->prev_state = state;
}
/* MIDI clock events affect channel 0 only */
if (midi_clock_port) {
void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes);
if (midi_clock_buf) {
jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf);
jack_midi_event_t cev;
for (jack_nframes_t j = 0; j < n_clock_events; j++) {
if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0)
continue;
if (cev.size >= 1) {
unsigned char msg = cev.buffer[0];
switch (msg) {
case 0xFA: {
struct channel_t *cur = atomic_load(&channels);
int sc_idx = cur[0].current_scene;
int s = atomic_load(&cur[0].scenes[sc_idx].state);
if (s == STATE_IDLE)
atomic_store(&cur[0].scenes[sc_idx].state, STATE_RECORD);
break;
}
case 0xFC: {
struct channel_t *cur = atomic_load(&channels);
int sc_idx = cur[0].current_scene;
atomic_store(&cur[0].scenes[sc_idx].state, STATE_IDLE);
break;
}
case 0xFB: {
struct channel_t *cur = atomic_load(&channels);
int sc_idx = cur[0].current_scene;
int s = atomic_load(&cur[0].scenes[sc_idx].state);
if (s == STATE_PAUSED)
atomic_store(&cur[0].scenes[sc_idx].state, STATE_LOOPING);
break;
}
default:
break;
}
}
}
}
}
atomic_fetch_add_explicit(&global_rt_cycles, 1, memory_order_release);
return 0;
}
/* ----------------------------------------------------------------
* shutdown callback
* ---------------------------------------------------------------- */
void jack_shutdown_cb(void *arg) {
(void)arg;
fprintf(stderr, "JACK shutdown\n");
exit(0);
}
/* ----------------------------------------------------------------
* looper initialisation
* ---------------------------------------------------------------- */
int looper_init(jack_client_t *client) {
queue_init(&cmd_queue);
queue_init(&cmd_queue_main_midi);
queue_init(&cmd_queue_main_fifo);
/* 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 */
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);
init[0].audio_out = jack_port_register(
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
if (!init[0].audio_in || !init[0].audio_out) {
fprintf(stderr, "Could not create audio ports for channel 0\n");
return -1;
}
atomic_store(&channel_count, 1);
midi_control_port = jack_port_register(
client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
midi_clock_port = jack_port_register(client, "clock", JACK_DEFAULT_MIDI_TYPE,
JackPortIsInput, 0);
if (!midi_control_port || !midi_clock_port) {
fprintf(stderr, "Could not create MIDI ports\n");
return -1;
}
return 0;
}
/* ----------------------------------------------------------------
* mainloop command processing
* ---------------------------------------------------------------- */
void looper_process_commands(jack_client_t *client) {
/* Drain mainloop command queues (add/remove) */
command_t cmd;
while (queue_pop(&cmd_queue_main_midi, &cmd)) {
switch (cmd.type) {
case CMD_ADD_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int idx;
for (idx = 0; idx < cap; idx++)
if (!atomic_load(&(get_channels_array()[idx].active)))
break;
if (idx == cap) {
if (ensure_capacity(client, idx) != 0)
break;
}
channel_add(client, idx);
break;
}
case CMD_ADD_MIDI_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int idx;
for (idx = 0; idx < cap; idx++)
if (!atomic_load(&(get_channels_array()[idx].active)))
break;
if (idx == cap) {
if (ensure_capacity(client, idx) != 0)
break;
}
channel_add_midi(client, idx);
break;
}
case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int remove_idx = -1;
for (int idx = 1; idx < cap; idx++)
if (atomic_load(&(get_channels_array()[idx].active)))
remove_idx = idx;
if (remove_idx != -1) {
channel_remove(client, remove_idx);
pending_unregister_idx = remove_idx;
pending_unregister_cycle = atomic_load(&global_rt_cycles);
}
break;
}
case CMD_ADD_SCENE: {
int cap = atomic_load(&channel_capacity);
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);
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);
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);
int bind = atomic_load(&bind_channel);
int ch = bind;
if (ch < cap) {
channel_prev_scene(client, ch);
}
break;
}
default:
break;
}
}
while (queue_pop(&cmd_queue_main_fifo, &cmd)) {
switch (cmd.type) {
case CMD_ADD_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int idx;
for (idx = 0; idx < cap; idx++)
if (!atomic_load(&(get_channels_array()[idx].active)))
break;
if (idx == cap) {
if (ensure_capacity(client, idx) != 0)
break;
}
channel_add(client, idx);
break;
}
case CMD_ADD_MIDI_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int idx;
for (idx = 0; idx < cap; idx++)
if (!atomic_load(&(get_channels_array()[idx].active)))
break;
if (idx == cap) {
if (ensure_capacity(client, idx) != 0)
break;
}
channel_add_midi(client, idx);
break;
}
case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity);
int remove_idx = -1;
for (int idx = 1; idx < cap; idx++)
if (atomic_load(&(get_channels_array()[idx].active)))
remove_idx = idx;
if (remove_idx != -1) {
channel_remove(client, remove_idx);
pending_unregister_idx = remove_idx;
pending_unregister_cycle = atomic_load(&global_rt_cycles);
}
break;
}
case CMD_ADD_SCENE: {
int cap = atomic_load(&channel_capacity);
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);
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);
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);
int bind = atomic_load(&bind_channel);
int ch = bind;
if (ch < cap) {
channel_prev_scene(client, ch);
}
break;
}
default:
break;
}
}
/* Deferred port unregistration wait until RT thread has seen active=0 */
if (pending_unregister_idx != -1) {
int current_cycle = atomic_load(&global_rt_cycles);
if (current_cycle - pending_unregister_cycle >= 1) {
int idx = pending_unregister_idx;
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);
if (cur[idx].midi_in)
jack_port_unregister(client, cur[idx].midi_in);
if (cur[idx].midi_out)
jack_port_unregister(client, cur[idx].midi_out);
pending_unregister_idx = -1;
}
}
/* Deferred free of old channel array wait until RT thread has seen new
* pointer */
if (pending_old != NULL) {
int current_cycle = atomic_load(&global_rt_cycles);
if (current_cycle - pending_old_cycle >= 1) {
free(pending_old);
pending_old = NULL;
}
}
}