// cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "midi.h" #include "wav.h" #include #include #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; atomic_int cmd_load = 0; atomic_int cmd_save = 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; /* writer thread function and sample rate holder */ static void *writer_thread(void *arg); static int global_sample_rate = 0; /* ---------------------------------------------------------------- * 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 != atomic_load(&channels[c].prev_state)) { switch (state) { case STATE_RECORD: atomic_store(&channels[c].record_pos, 0); atomic_store(&channels[c].loop_count, 0); break; case STATE_LOOPING: if (atomic_load(&channels[c].prev_state) == STATE_RECORD && atomic_load(&channels[c].record_pos) > 0) atomic_store(&channels[c].loop_count, atomic_load(&channels[c].record_pos)); atomic_store(&channels[c].playback_pos, 0); break; default: break; } } 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++) { int rp = atomic_fetch_add(&channels[c].record_pos, 1); if (rp < LOOP_BUF_SIZE) channels[c].loop_buffer[rp] = f_in[i]; f_out[i] = f_in[i]; } } else { memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } break; case STATE_LOOPING: int lc = atomic_load(&channels[c].loop_count); if (lc > 0) { float *outf = (float *)out; for (i = 0; i < nframes; i++) { int pp = atomic_load(&channels[c].playback_pos); outf[i] = channels[c].loop_buffer[pp]; atomic_store(&channels[c].playback_pos, (pp + 1) % lc); } } 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; } // push loop output into save ring if saving (atomic load) RingBuf *r = (RingBuf *)atomic_load_explicit(&channels[c].save_ring, memory_order_acquire); if (r != NULL) { if (state == STATE_LOOPING && atomic_load(&channels[c].loop_count) > 0) { const float *outf = (const float *)out; ring_write(r, outf, nframes); } } atomic_store(&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) { /* store sample rate for writer thread */ global_sample_rate = jack_get_sample_rate(client); /* channel 0 */ channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE); atomic_store(&channels[0].prev_state, -1); channels[0].loop_count = 0; atomic_store(&channels[0].record_pos, 0); atomic_store(&channels[0].playback_pos, 0); atomic_store_explicit(&channels[0].save_ring, NULL, memory_order_release); 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; } /* ---------------------------------------------------------------- * writer thread – consumes the save ring and writes WAV file * ---------------------------------------------------------------- */ static void *writer_thread(void *arg) { struct channel_t *ch = (struct channel_t *)arg; RingBuf *ring = (RingBuf *)ch->save_ring; if (!ring) return NULL; static const char *path = "save.wav"; unsigned sr = (unsigned)global_sample_rate; if (sr == 0) sr = 48000; int lc = atomic_load(&ch->loop_count); float *outbuf = malloc((size_t)lc * sizeof(float)); if (!outbuf) { ring_destroy(ring); free(ring); ch->save_ring = NULL; return NULL; } size_t collected = 0; size_t want = (size_t)lc; while (collected < want) { size_t got = ring_read(ring, outbuf + collected, want - collected); collected += got; if (got == 0) { struct timespec req = {.tv_sec = 0, .tv_nsec = 10000000}; nanosleep(&req, NULL); } } wav_write(path, outbuf, (unsigned)lc, sr); free(outbuf); ring_destroy(ring); free(ring); atomic_store_explicit(&ch->save_ring, NULL, memory_order_release); return NULL; } /* ---------------------------------------------------------------- * 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; } } /* ---------- load command ---------- */ if (atomic_exchange(&cmd_load, 0)) { float *buf = NULL; unsigned frames = 0; printf("LOAD: wav_read called\n"); if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) { printf("LOAD: success, frames=%u\n", frames); if (frames > LOOP_BUF_SIZE) frames = LOOP_BUF_SIZE; memcpy(channels[0].loop_buffer, buf, frames * sizeof(float)); atomic_store(&channels[0].loop_count, (int)frames); atomic_store(&channels[0].record_pos, 0); atomic_store(&channels[0].playback_pos, 0); atomic_store(&channels[0].state, STATE_LOOPING); atomic_store(&channels[0].prev_state, -1); free(buf); } else { fprintf(stderr, "Failed to load loop.wav\n"); printf("LOAD: FAILED\n"); } } /* ---------- save command (writer thread) ---------- */ if (atomic_exchange(&cmd_save, 0)) { int lc = atomic_load(&channels[0].loop_count); if (atomic_load(&channels[0].state) == STATE_LOOPING && lc > 0 && channels[0].save_ring == NULL) { RingBuf *ring = (RingBuf *)malloc(sizeof(RingBuf)); if (ring) { size_t sz = (size_t)lc * 2; if (ring_init(ring, sz) == 0) { atomic_store_explicit(&channels[0].save_ring, (_Atomic RingBuf *)ring, memory_order_release); pthread_t th; pthread_create(&th, NULL, writer_thread, &channels[0]); pthread_detach(th); } else { free(ring); } } } } }