// cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "command.h" #include "midi.h" #include "queue.h" #include #include #include #include #include #include #include /* 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) { 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 < 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 RT‑safe 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 s = atomic_load(&cur[0].state); if (s == STATE_IDLE) atomic_store(&cur[0].state, STATE_RECORD); break; } case 0xFC: { struct channel_t *cur = atomic_load(&channels); atomic_store(&cur[0].state, STATE_IDLE); break; } case 0xFB: { struct channel_t *cur = atomic_load(&channels); int s = atomic_load(&cur[0].state); if (s == STATE_PAUSED) atomic_store(&cur[0].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; } /* ---------------------------------------------------------------- * main‑loop command processing * ---------------------------------------------------------------- */ void looper_process_commands(jack_client_t *client) { /* Drain main‑loop 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); struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) if (!atomic_load(&cur[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); struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) if (!atomic_load(&cur[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); struct channel_t *cur = get_channels_array(); int remove_idx = -1; 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); pending_unregister_idx = remove_idx; pending_unregister_cycle = atomic_load(&global_rt_cycles); } 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; } } 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 < cap; idx++) if (!atomic_load(&cur[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); struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) if (!atomic_load(&cur[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); struct channel_t *cur = get_channels_array(); int remove_idx = -1; 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); pending_unregister_idx = remove_idx; pending_unregister_cycle = atomic_load(&global_rt_cycles); } 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; } } /* 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; } } }