Files
looper/engine/src/looper.c~
2026-05-17 19:39:54 +00:00

676 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"
<<<<<<< HEAD
#include "wav.h"
#include "ringbuffer.h"
#include "pipe.h"
#include <jack/jack.h>
#include <sys/stat.h>
=======
#include "queue.h"
>>>>>>> 3-integrate-carla
#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>
<<<<<<< HEAD
#include <time.h>
#include "queue.h"
#include "command.h"
/* Global command queues */
spsc_queue_t cmd_queue;
spsc_queue_t cmd_queue_main_midi;
spsc_queue_t cmd_queue_main_fifo;
=======
#include <sys/stat.h>
#include <unistd.h>
>>>>>>> 3-integrate-carla
#define STATUS_FIFO "/tmp/looper_status"
/* writer status fd */
static int status_fd = -1;
static void looper_write_status(void) {
<<<<<<< HEAD
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;
}
}
=======
int fd = open(STATUS_FIFO, O_WRONLY | O_NONBLOCK);
if (fd < 0)
return;
struct channel_t *cur = get_channels_array();
int cap = atomic_load(&channel_capacity);
char buf[256];
for (int ch = 0; ch < cap; ch++) {
if (!atomic_load(&cur[ch].active))
continue;
int sc_idx = atomic_load(&cur[ch].current_scene);
int state = atomic_load(&cur[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(fd, buf, n);
(void)ret;
}
}
close(fd);
>>>>>>> 3-integrate-carla
}
/* 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;
/* writer thread function and sample rate holder */
static void *writer_thread(void *arg);
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 = 0;
switch (cmd.type) {
case CMD_CYCLE: {
int state = atomic_load(&channels[ch].state);
switch (state) {
case STATE_IDLE:
atomic_store(&channels[ch].state, STATE_RECORD);
break;
case STATE_RECORD:
atomic_store(&channels[ch].state, STATE_LOOPING);
break;
case STATE_LOOPING:
atomic_store(&channels[ch].state, STATE_PAUSED);
break;
case STATE_PAUSED:
atomic_store(&channels[ch].state, STATE_LOOPING);
break;
}
atomic_store(&channels[ch].prev_state, -1);
break;
}
case CMD_STOP:
atomic_store(&channels[ch].state, STATE_IDLE);
atomic_store(&channels[ch].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:
case CMD_REMOVE_SCENE:
case CMD_NEXT_SCENE:
case CMD_PREV_SCENE:
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;
}
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;
}
}
<<<<<<< HEAD
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 {
=======
if (active_channels[c].type == CHANNEL_MIDI) {
/* MIDI channel handling */
switch (state) {
case STATE_RECORD: {
void *midi_in_buf =
jack_port_get_buffer(active_channels[c].midi_in, nframes);
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 */
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_out_buf) {
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: {
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_out_buf) {
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:
/* no output */
break;
default: /* IDLE */
{
void *midi_in_buf =
jack_port_get_buffer(active_channels[c].midi_in, nframes);
void *midi_out_buf =
jack_port_get_buffer(active_channels[c].midi_out, nframes);
if (midi_in_buf && midi_out_buf) {
jack_midi_clear_buffer(midi_out_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;
}
if (state == STATE_LOOPING) {
atomic_store(&sc->loop_count, atomic_load(&sc->record_pos));
}
} else {
/* audio channel handling */
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_load(&sc->record_pos);
if (rp < LOOP_BUF_SIZE) {
sc->loop.audio_buffer[rp] = f_in[i];
atomic_store(&sc->record_pos, rp + 1);
}
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 (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:
>>>>>>> 3-integrate-carla
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);
/* 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;
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;
}
/* ----------------------------------------------------------------
* 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;
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);
}
}
}
}
/* write current state to status FIFO */
looper_write_status();
}