// cppcheck-suppress missingIncludeSystem #include "looper.h" #include "channel.h" #include "midi.h" #include "pipe.h" #include "queue.h" #include "wav.h" #include #include #include #include #include #include #include #include #include #include #include /* Global command queues (used by midi.c and pipe.c) */ spsc_queue_t cmd_queue; spsc_queue_t cmd_queue_main_midi; spsc_queue_t cmd_queue_main_fifo; #define STATUS_FIFO "/tmp/looper_status" /* writer status fd */ static int status_fd = -1; static void looper_write_status(void) { if (status_fd < 0) return; char buf[256]; for (int ch = 0; ch < MAX_CHANNELS; ch++) { if (!atomic_load(&channels[ch].active)) continue; int sc_idx = atomic_load(&channels[ch].current_scene); int state = atomic_load(&channels[ch].scenes[sc_idx].state); const char *state_str; switch (state) { case STATE_IDLE: state_str = "IDLE"; break; case STATE_RECORD: state_str = "RECORD"; break; case STATE_LOOPING: state_str = "LOOPING"; break; case STATE_PAUSED: state_str = "PAUSED"; break; default: state_str = "UNKNOWN"; } int n = snprintf(buf, sizeof(buf), "CH=%d SC=%d STATE=%s\n", ch, sc_idx, state_str); if (n > 0) { int ret = write(status_fd, buf, n); (void)ret; } } } /* Global state (shared across files) */ struct channel_t channels[MAX_CHANNELS]; atomic_int channel_count = 0; atomic_int channel_capacity = MAX_CHANNELS; 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; /* sample rate holder */ static int global_sample_rate = 0; /* execute a single command (called from looper_process_commands) */ static void exec_command(command_t cmd, jack_client_t *client) { int ch = cmd.channel; if (ch < 0 || ch >= MAX_CHANNELS) ch = 0; switch (cmd.type) { case CMD_CYCLE: { int ch = cmd.channel; if (ch < 0 || ch >= MAX_CHANNELS) ch = 0; // Save the desired scene (may have been set by CMD_SET_SCENE) int requested_scene = atomic_load(&channels[ch].current_scene); // Clamp requested_scene to valid range if (requested_scene < 0) requested_scene = 0; if (requested_scene >= MAX_SCENES) requested_scene = MAX_SCENES - 1; // Auto-create channel if it doesn't exist if (!channels[ch].active) { channel_add(client, ch); } // Ensure enough scenes exist to satisfy requested_scene int sc_count = atomic_load(&channels[ch].scene_count); while (requested_scene >= sc_count && sc_count < MAX_SCENES) { channel_add_scene(client, ch); sc_count = atomic_load(&channels[ch].scene_count); } // Clamp requested_scene if MAX_SCENES prevents adding enough scenes if (requested_scene >= sc_count) requested_scene = sc_count - 1; // Restore the requested scene (channel_add or add_scene may have reset current_scene) atomic_store(&channels[ch].current_scene, requested_scene); int sc_idx = atomic_load(&channels[ch].current_scene); scene_t *sc_ptr = &channels[ch].scenes[sc_idx]; int state = atomic_load(&sc_ptr->state); fprintf(stderr, "CMD_CYCLE: ch=%d old_state=%d\n", ch, state); switch (state) { case STATE_IDLE: atomic_store(&sc_ptr->state, STATE_RECORD); break; case STATE_RECORD: atomic_store(&sc_ptr->state, STATE_LOOPING); break; case STATE_LOOPING: atomic_store(&sc_ptr->state, STATE_PAUSED); break; case STATE_PAUSED: atomic_store(&sc_ptr->state, STATE_LOOPING); break; } break; } case CMD_STOP: for (int s = 0; s < atomic_load(&channels[ch].scene_count); s++) { atomic_store(&channels[ch].scenes[s].state, STATE_IDLE); atomic_store(&channels[ch].scenes[s].prev_state, -1); } break; case CMD_ADD_CHANNEL: case CMD_ADD_MIDI_CHANNEL: { int idx; for (idx = 0; idx < MAX_CHANNELS; idx++) if (!channels[idx].active) break; if (idx < MAX_CHANNELS) channel_add(client, idx); break; } case CMD_REMOVE_CHANNEL: { int remove_idx = -1; for (int idx = 1; idx < MAX_CHANNELS; idx++) if (channels[idx].active) remove_idx = idx; if (remove_idx != -1) { channel_remove(client, remove_idx); pending_unregister_idx = remove_idx; } break; } case CMD_BIND_CHANNEL: atomic_store(&bind_channel, cmd.data); break; case CMD_UNBIND: atomic_store(&bind_channel, 0); break; case CMD_LOAD: atomic_store(&cmd_load, 1); break; case CMD_SAVE: atomic_store(&cmd_save, 1); break; case CMD_ADD_SCENE: channel_add_scene(client, ch); break; case CMD_REMOVE_SCENE: channel_remove_scene(client, ch); break; case CMD_NEXT_SCENE: channel_next_scene(client, ch); break; case CMD_PREV_SCENE: channel_prev_scene(client, ch); break; case CMD_SET_SCENE: { int sc = cmd.data; // Allow any scene index; scenes will be added by CMD_CYCLE if needed if (sc >= 0) { atomic_store(&channels[ch].current_scene, sc); } 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); } } /* 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; } /* For each channel, use the current scene */ int sc_idx = atomic_load(&channels[c].current_scene); scene_t *sc = &channels[c].scenes[sc_idx]; int state = atomic_load(&sc->state); int prev = atomic_load(&sc->prev_state); if (state != prev) { switch (state) { case STATE_RECORD: atomic_store(&sc->record_pos, 0); atomic_store(&sc->loop_count, 0); break; case STATE_LOOPING: if (prev == STATE_RECORD && atomic_load(&sc->record_pos) > 0) atomic_store(&sc->loop_count, atomic_load(&sc->record_pos)); atomic_store(&sc->playback_pos, 0); break; default: break; } } /* Handle MIDI channels separately */ if (channels[c].type == CHANNEL_MIDI) { /* MIDI channel handling */ void *midi_in_buf = jack_port_get_buffer(channels[c].midi_in, nframes); void *midi_out_buf = jack_port_get_buffer(channels[c].midi_out, nframes); if (!midi_out_buf) continue; switch (state) { case STATE_RECORD: { 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; int rp = atomic_load(&sc->record_pos); if (rp < MAX_MIDI_EVENTS) { sc->loop.midi_events[rp].timestamp = ev.time; sc->loop.midi_events[rp].status = ev.buffer[0]; sc->loop.midi_events[rp].note = (ev.size > 1) ? ev.buffer[1] : 0; sc->loop.midi_events[rp].velocity = (ev.size > 2) ? ev.buffer[2] : 0; atomic_store(&sc->record_pos, rp + 1); } } /* forward incoming MIDI to output during record */ 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: { jack_midi_clear_buffer(midi_out_buf); int cnt = atomic_load(&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: jack_midi_clear_buffer(midi_out_buf); break; default: /* IDLE */ jack_midi_clear_buffer(midi_out_buf); 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; jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size); } } break; } continue; } /* Audio channel handling */ 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; if (c == 0 && !atomic_load(&channels[c].active)) { fprintf(stderr, "CHANNEL0_NOT_ACTIVE during record\n"); } switch (state) { case STATE_RECORD: if (c == 0 && atomic_load(&sc->record_pos) == 0) { if (in) { fprintf(stderr, "FIRST_INPUT_SAMPLE = %f\n", ((const float*)in)[0]); } else { fprintf(stderr, "FIRST_INPUT_SAMPLE = NULL\n"); } } if (in) { float *f_out = (float *)out; const float *f_in = (const float *)in; for (jack_nframes_t i = 0; i < nframes; i++) { int rp = atomic_fetch_add(&sc->record_pos, 1); if (rp < LOOP_BUF_SIZE) sc->loop.audio_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 loop_cnt = atomic_load(&sc->loop_count); if (loop_cnt > 0) { float *outf = (float *)out; int pp = atomic_load(&sc->playback_pos); for (jack_nframes_t i = 0; i < nframes; i++) { outf[i] = sc->loop.audio_buffer[pp]; pp = (pp + 1) % loop_cnt; } atomic_store(&sc->playback_pos, pp); } 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 && !atomic_load(&channels[c].save_complete)) { if (state == STATE_LOOPING && atomic_load(&sc->loop_count) > 0) { const float *outf = (const float *)out; ring_write(r, outf, nframes); } } atomic_store(&sc->prev_state, state); } /* MIDI clock events – affect current scene of channel 0 */ 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 sc0 = atomic_load(&channels[0].current_scene); int s = atomic_load(&channels[0].scenes[sc0].state); if (s == STATE_IDLE) atomic_store(&channels[0].scenes[sc0].state, STATE_RECORD); break; } case 0xFC: { int sc0 = atomic_load(&channels[0].current_scene); atomic_store(&channels[0].scenes[sc0].state, STATE_IDLE); break; } case 0xFB: { int sc0 = atomic_load(&channels[0].current_scene); int s = atomic_load(&channels[0].scenes[sc0].state); if (s == STATE_PAUSED) atomic_store(&channels[0].scenes[sc0].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); /* create status FIFO (ignore if already exists) */ mkfifo(STATUS_FIFO, 0666); /* open the status FIFO for reading+writing so writes work even without reader */ status_fd = open(STATUS_FIFO, O_RDWR); if (status_fd < 0) { perror("open status FIFO"); } queue_init(&cmd_queue); queue_init(&cmd_queue_main_midi); queue_init(&cmd_queue_main_fifo); /* start the FIFO reader thread */ pipe_start_reader(); /* channel 0 */ channels[0].active = 1; channels[0].type = CHANNEL_AUDIO; /* default */ channels[0].current_scene = 0; channels[0].scene_count = 1; init_scene(&channels[0].scenes[0]); /* sets state IDLE, prev_state -1 */ atomic_store(&channels[0].scenes[0].loop_count, 0); atomic_store(&channels[0].scenes[0].record_pos, 0); atomic_store(&channels[0].scenes[0].playback_pos, 0); atomic_store_explicit(&channels[0].save_ring, NULL, memory_order_release); atomic_store(&channels[0].save_complete, 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; } /* Give JACK time to register the ports before clients connect */ { struct timespec req = {.tv_sec = 0, .tv_nsec = 500000000}; nanosleep(&req, NULL); } return 0; } /* ---------------------------------------------------------------- * main‑loop command processing * ---------------------------------------------------------------- */ void looper_process_commands(jack_client_t *client) { /* process commands from the three queues FIRST */ command_t cmd; while (queue_pop(&cmd_queue, &cmd)) exec_command(cmd, client); while (queue_pop(&cmd_queue_main_midi, &cmd)) exec_command(cmd, client); while (queue_pop(&cmd_queue_main_fifo, &cmd)) exec_command(cmd, 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; } /* ---------- add channel ---------- */ 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); } } /* ---------- remove channel ---------- */ 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; fprintf(stderr, "LOAD: wav_read called\n"); if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) { fprintf(stderr, "LOAD: success, frames=%u\n", frames); int sc_idx = atomic_load(&channels[0].current_scene); scene_t *sc = &channels[0].scenes[sc_idx]; if (frames > LOOP_BUF_SIZE) frames = LOOP_BUF_SIZE; memcpy(sc->loop.audio_buffer, buf, frames * sizeof(float)); atomic_store(&sc->loop_count, (int)frames); atomic_store(&sc->record_pos, 0); atomic_store(&sc->playback_pos, 0); atomic_store(&sc->state, STATE_LOOPING); atomic_store(&sc->prev_state, -1); free(buf); } else { fprintf(stderr, "Failed to load loop.wav\n"); fprintf(stderr, "LOAD: FAILED\n"); } } /* ---------- save command (synchronous) ---------- */ if (atomic_exchange(&cmd_save, 0)) { int sc_idx = atomic_load(&channels[0].current_scene); scene_t *sc = &channels[0].scenes[sc_idx]; int lc = atomic_load(&sc->loop_count); int rp = atomic_load(&sc->record_pos); int state = atomic_load(&sc->state); printf("SAVE debug: state=%d loop_count=%d record_pos=%d\n", state, lc, rp); /* Allow save from any state where we have data */ int frames_to_save = 0; if ((state == STATE_LOOPING || state == STATE_PAUSED) && lc > 0) { frames_to_save = lc; } else if (state == STATE_RECORD && rp > 0) { frames_to_save = rp; } if (frames_to_save > 0) { /* Deactivate channel to prevent RT thread from reading the buffer */ int was_active = atomic_load(&channels[0].active); if (was_active) { atomic_store(&channels[0].active, 0); struct timespec req = {.tv_sec = 0, .tv_nsec = 500000000}; /* 500 ms */ nanosleep(&req, NULL); } /* Now safe to copy the loop buffer */ float *data = malloc((size_t)frames_to_save * sizeof(float)); if (data) { memcpy(data, sc->loop.audio_buffer, (size_t)frames_to_save * sizeof(float)); unsigned sr = (unsigned)global_sample_rate; if (sr == 0) sr = 48000; char save_path[256]; snprintf(save_path, sizeof(save_path), "save.wav"); printf("SAVE: writing %u frames, first sample = %f\n", (unsigned)frames_to_save, data[0]); int ret = wav_write(save_path, data, (unsigned)frames_to_save, sr); printf("SAVE: wav_write returned %d\n", ret); free(data); } /* Reactivate channel – use a shorter sleep to reduce xrun risk */ if (was_active) { struct timespec req = {.tv_sec = 0, .tv_nsec = 200000000}; /* 200 ms */ nanosleep(&req, NULL); atomic_store(&channels[0].active, 1); } } else { printf("SAVE: condition not met, state=%d lc=%d rp=%d\n", state, lc, rp); } } /* write current state to status FIFO */ looper_write_status(); }