// cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "midi.h" #include #include #include #include #include #include #include /* Global state (shared across files) */ struct channel_t channels[MAX_CHANNELS]; atomic_int channel_count = 0; int next_channel_id = 1; atomic_int cmd_add = 0; atomic_int cmd_remove = 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; /* Deferred removal index (1 second grace) */ static int pending_unregister_idx = -1; /* ---------------------------------------------------------------- * 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); } } /* process each active channel */ for (int c = 0; c < MAX_CHANNELS; c++) { if (!atomic_load(&channels[c].active)) continue; /* Guard against NULL ports (e.g. if port registration failed) */ if (!channels[c].audio_in || !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); jack_default_audio_sample_t *out = (jack_default_audio_sample_t *)jack_port_get_buffer( channels[c].audio_out, nframes); if (!out) continue; int state = atomic_load(&channels[c].state); if (state != channels[c].prev_state) { switch (state) { case STATE_RECORD: channels[c].record_pos = 0; 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; break; default: break; } } jack_nframes_t i; switch (state) { case STATE_RECORD: if (in) { for (i = 0; i < nframes; i++) { if (channels[c].record_pos < LOOP_BUF_SIZE) channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i]; ((float *)out)[i] = ((const float *)in)[i]; // cppcheck-suppress unreadVariable } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } break; case STATE_LOOPING: if (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; } } 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; } channels[c].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: { int s = atomic_load(&channels[0].state); if (s == STATE_IDLE) atomic_store(&channels[0].state, STATE_RECORD); break; } case 0xFC: atomic_store(&channels[0].state, STATE_IDLE); break; case 0xFB: { int s = atomic_load(&channels[0].state); if (s == STATE_PAUSED) atomic_store(&channels[0].state, STATE_LOOPING); break; } default: break; } } } } } 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) { /* 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( client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); channels[0].audio_out = jack_port_register( client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if (!channels[0].audio_in || !channels[0].audio_out) { fprintf(stderr, "Could not create audio ports for channel 0\n"); return -1; } 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) { /* Unregister any ports that were marked for deferred removal. By now the real‑time thread has had at least one full cycle to see the `active = 0` store. */ if (pending_unregister_idx != -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); pending_unregister_idx = -1; } if (atomic_exchange(&cmd_add, 0)) { int idx; for (idx = 0; idx < MAX_CHANNELS; idx++) if (!channels[idx].active) break; if (idx < MAX_CHANNELS) { channel_add(client, idx); } } if (atomic_exchange(&cmd_remove, 0)) { int remove_idx = -1; for (int idx = 1; idx < MAX_CHANNELS; idx++) if (channels[idx].active) remove_idx = idx; if (remove_idx != -1) { /* Mark inactive now; ports will be unregistered next round */ channel_remove(client, remove_idx); pending_unregister_idx = remove_idx; } } }