Files
looper/engine/src/looper.c

648 lines
20 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 "midi.h"
#include "pipe.h"
#include "queue.h"
#include "wav.h"
#include <fcntl.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* 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;
}
/* ----------------------------------------------------------------
* mainloop 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 realtime 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();
}