Merge branch '3-integrate-carla'

This commit is contained in:
Loic Coenen
2026-05-17 19:39:54 +00:00
42 changed files with 2716 additions and 340 deletions

View File

@@ -2,13 +2,11 @@
#include "looper.h"
#include "channel.h"
#include "midi.h"
#include "queue.h"
#include "wav.h"
#include "ringbuffer.h"
#include "pipe.h"
#include <jack/jack.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <pthread.h>
@@ -16,11 +14,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "queue.h"
#include "command.h"
#include <sys/stat.h>
#include <unistd.h>
/* Global command queues */
/* 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;
@@ -31,29 +28,28 @@ spsc_queue_t cmd_queue_main_fifo;
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 state_val = atomic_load(&channels[ch].state);
const char *state_str;
switch (state_val) {
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, 0, state_str);
if (n > 0) {
int ret = write(status_fd, buf, n);
(void)ret;
}
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) */
@@ -84,27 +80,30 @@ static void exec_command(command_t cmd, jack_client_t *client) {
switch (cmd.type) {
case CMD_CYCLE: {
int state = atomic_load(&channels[ch].state);
int sc_idx = atomic_load(&channels[ch].current_scene);
int state = atomic_load(&channels[ch].scenes[sc_idx].state);
switch (state) {
case STATE_IDLE:
atomic_store(&channels[ch].state, STATE_RECORD);
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_RECORD);
break;
case STATE_RECORD:
atomic_store(&channels[ch].state, STATE_LOOPING);
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_LOOPING);
break;
case STATE_LOOPING:
atomic_store(&channels[ch].state, STATE_PAUSED);
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_PAUSED);
break;
case STATE_PAUSED:
atomic_store(&channels[ch].state, STATE_LOOPING);
atomic_store(&channels[ch].scenes[sc_idx].state, STATE_LOOPING);
break;
}
atomic_store(&channels[ch].prev_state, -1);
atomic_store(&channels[ch].scenes[sc_idx].prev_state, -1);
break;
}
case CMD_STOP:
atomic_store(&channels[ch].state, STATE_IDLE);
atomic_store(&channels[ch].prev_state, -1);
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:
@@ -147,9 +146,19 @@ static void exec_command(command_t cmd, jack_client_t *client) {
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;
default:
@@ -181,45 +190,113 @@ int process_callback(jack_nframes_t nframes, void *arg) {
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;
/* 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);
int state = atomic_load(&channels[c].state);
if (state != atomic_load(&channels[c].prev_state)) {
if (state != prev) {
switch (state) {
case STATE_RECORD:
atomic_store(&channels[c].record_pos, 0);
atomic_store(&channels[c].loop_count, 0);
atomic_store(&sc->record_pos, 0);
atomic_store(&sc->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);
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;
}
}
jack_nframes_t i;
/* 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;
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);
for (jack_nframes_t i = 0; i < nframes; i++) {
int rp = atomic_fetch_add(&sc->record_pos, 1);
if (rp < LOOP_BUF_SIZE)
channels[c].loop_buffer[rp] = f_in[i];
sc->loop.audio_buffer[rp] = f_in[i];
f_out[i] = f_in[i];
}
} else {
@@ -227,19 +304,21 @@ int process_callback(jack_nframes_t nframes, void *arg) {
}
break;
case STATE_LOOPING:
int lc = atomic_load(&channels[c].loop_count);
if (lc > 0) {
case STATE_LOOPING: {
int loop_cnt = atomic_load(&sc->loop_count);
if (loop_cnt > 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);
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);
@@ -254,20 +333,20 @@ int process_callback(jack_nframes_t nframes, void *arg) {
break;
}
// push loop output into save ring if saving (atomic load)
/* 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) {
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(&channels[c].prev_state, state);
atomic_store(&sc->prev_state, state);
}
/* MIDI clock events affect channel 0 only */
/* 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) {
@@ -280,18 +359,22 @@ int process_callback(jack_nframes_t nframes, void *arg) {
unsigned char msg = cev.buffer[0];
switch (msg) {
case 0xFA: {
int s = atomic_load(&channels[0].state);
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].state, STATE_RECORD);
atomic_store(&channels[0].scenes[sc0].state, STATE_RECORD);
break;
}
case 0xFC:
atomic_store(&channels[0].state, STATE_IDLE);
case 0xFC: {
int sc0 = atomic_load(&channels[0].current_scene);
atomic_store(&channels[0].scenes[sc0].state, STATE_IDLE);
break;
}
case 0xFB: {
int s = atomic_load(&channels[0].state);
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].state, STATE_LOOPING);
atomic_store(&channels[0].scenes[sc0].state, STATE_LOOPING);
break;
}
default:
@@ -341,12 +424,15 @@ int looper_init(jack_client_t *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);
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);
@@ -375,6 +461,8 @@ int looper_init(jack_client_t *client) {
* ---------------------------------------------------------------- */
static void *writer_thread(void *arg) {
struct channel_t *ch = (struct channel_t *)arg;
int sc_idx = atomic_load(&ch->current_scene);
scene_t *sc = &ch->scenes[sc_idx];
RingBuf *ring = (RingBuf *)ch->save_ring;
if (!ring)
return NULL;
@@ -384,7 +472,7 @@ static void *writer_thread(void *arg) {
if (sr == 0)
sr = 48000;
int lc = atomic_load(&ch->loop_count);
int lc = atomic_load(&sc->loop_count);
float *outbuf = malloc((size_t)lc * sizeof(float));
if (!outbuf) {
ring_destroy(ring);
@@ -405,6 +493,12 @@ static void *writer_thread(void *arg) {
wav_write(path, outbuf, (unsigned)lc, sr);
free(outbuf);
/* Signal the RT thread to stop writing */
atomic_store_explicit(&ch->save_complete, 1, memory_order_release);
/* Wait for the RT thread to see the flag (one audio period) */
struct timespec req = { .tv_sec = 0, .tv_nsec = 10000000 }; /* 10ms */
nanosleep(&req, NULL);
ring_destroy(ring);
free(ring);
atomic_store_explicit(&ch->save_ring, NULL, memory_order_release);
@@ -467,14 +561,16 @@ void looper_process_commands(jack_client_t *client) {
printf("LOAD: wav_read called\n");
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
printf("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(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);
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");
@@ -484,8 +580,10 @@ void looper_process_commands(jack_client_t *client) {
/* ---------- 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 &&
int sc_idx = atomic_load(&channels[0].current_scene);
scene_t *sc = &channels[0].scenes[sc_idx];
int lc = atomic_load(&sc->loop_count);
if (atomic_load(&sc->state) == STATE_LOOPING && lc > 0 &&
channels[0].save_ring == NULL) {
RingBuf *ring = (RingBuf *)malloc(sizeof(RingBuf));
if (ring) {