From ff226a8ea6a6257371eb89b1adb949108ae3d45d Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Sun, 10 May 2026 11:29:41 +0000 Subject: [PATCH] feat: add per-channel MIDI looping support Co-authored-by: aider (deepseek/deepseek-reasoner) --- src/channel.c | 31 +++++++++ src/channel.h | 30 +++++++-- src/command.h | 1 + src/looper.c | 179 +++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 199 insertions(+), 42 deletions(-) diff --git a/src/channel.c b/src/channel.c index 542d63a..079458b 100644 --- a/src/channel.c +++ b/src/channel.c @@ -29,6 +29,37 @@ void channel_add(jack_client_t *client, int idx) { cur[idx].loop_count = 0; cur[idx].record_pos = 0; cur[idx].playback_pos = 0; + cur[idx].type = CHANNEL_AUDIO; + + next_channel_id++; + atomic_fetch_add(&channel_count, 1); +} + +void channel_add_midi(jack_client_t *client, int idx) { + struct channel_t *cur = get_channels_array(); + + char in_name[64], out_name[64]; + snprintf(in_name, sizeof(in_name), "channel%d_midi_in", next_channel_id); + snprintf(out_name, sizeof(out_name), "channel%d_midi_out", next_channel_id); + + cur[idx].midi_in = jack_port_register( + client, in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + cur[idx].midi_out = jack_port_register( + client, out_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); + if (!cur[idx].midi_in || !cur[idx].midi_out) { + fprintf(stderr, "Failed to register MIDI ports for channel %d\n", + next_channel_id); + atomic_store(&cur[idx].active, 0); + return; + } + + atomic_store(&cur[idx].active, 1); + atomic_store(&cur[idx].state, STATE_IDLE); + cur[idx].prev_state = -1; + cur[idx].loop_count = 0; + cur[idx].record_pos = 0; + cur[idx].playback_pos = 0; + cur[idx].type = CHANNEL_MIDI; next_channel_id++; atomic_fetch_add(&channel_count, 1); diff --git a/src/channel.h b/src/channel.h index 7e706e7..abe775b 100644 --- a/src/channel.h +++ b/src/channel.h @@ -7,6 +7,20 @@ #define LOOP_BUF_SIZE (5 * 48000) +#define MAX_MIDI_EVENTS 1024 + +typedef enum { + CHANNEL_AUDIO, + CHANNEL_MIDI +} channel_type_t; + +typedef struct { + jack_nframes_t timestamp; /* frame offset relative to loop start */ + unsigned char status; + unsigned char note; + unsigned char velocity; +} midi_event_t; + typedef enum { STATE_IDLE, STATE_RECORD, @@ -15,15 +29,22 @@ typedef enum { } looper_state; struct channel_t { + channel_type_t type; /* CHANNEL_AUDIO or CHANNEL_MIDI */ + atomic_int state; int prev_state; - float loop_buffer[LOOP_BUF_SIZE]; - int loop_count; - int record_pos; - int playback_pos; + union { + float audio_buffer[LOOP_BUF_SIZE]; + midi_event_t midi_events[MAX_MIDI_EVENTS]; + } loop; + int loop_count; /* for audio: length in samples; for MIDI: number of recorded events */ + int record_pos; /* for audio: sample index; for MIDI: next event index for recording */ + int playback_pos; /* for audio: sample index; for MIDI: next event index for playback */ atomic_int active; jack_port_t *audio_in; jack_port_t *audio_out; + jack_port_t *midi_in; + jack_port_t *midi_out; }; /* Globals declared in looper.c */ @@ -39,5 +60,6 @@ static inline struct channel_t *get_channels_array(void) { void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); +void channel_add_midi(jack_client_t *client, int idx); #endif diff --git a/src/command.h b/src/command.h index d14dd9c..e6475d9 100644 --- a/src/command.h +++ b/src/command.h @@ -8,6 +8,7 @@ typedef enum { CMD_UNBIND, // reset bind to channel 0 CMD_ADD_CHANNEL, // add a new dynamic channel CMD_REMOVE_CHANNEL, // remove last dynamic channel + CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel } cmd_type_t; typedef struct { diff --git a/src/looper.c b/src/looper.c index 2e0f5e7..b57ac22 100644 --- a/src/looper.c +++ b/src/looper.c @@ -175,49 +175,124 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } - 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++) { - if (active_channels[c].record_pos < LOOP_BUF_SIZE) - active_channels[c].loop_buffer[active_channels[c].record_pos++] = - f_in[i]; - f_out[i] = f_in[i]; + 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; + if (active_channels[c].record_pos < MAX_MIDI_EVENTS) { + active_channels[c].loop.midi_events[active_channels[c].record_pos].timestamp = ev.time; + active_channels[c].loop.midi_events[active_channels[c].record_pos].status = ev.buffer[0]; + active_channels[c].loop.midi_events[active_channels[c].record_pos].note = (ev.size > 1) ? ev.buffer[1] : 0; + active_channels[c].loop.midi_events[active_channels[c].record_pos].velocity = (ev.size > 2) ? ev.buffer[2] : 0; + active_channels[c].record_pos++; + } + } + /* 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); + } + } } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + break; } - break; - - case STATE_LOOPING: - if (active_channels[c].loop_count > 0) { - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - outf[i] = - active_channels[c].loop_buffer[active_channels[c].playback_pos]; - active_channels[c].playback_pos = - (active_channels[c].playback_pos + 1) % - active_channels[c].loop_count; + 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 = active_channels[c].loop_count; /* number of recorded events */ + if (cnt > 0) { + /* simple: output all recorded events at frame 0 of each cycle */ + for (int e = 0; e < cnt; e++) { + unsigned char msg[3]; + msg[0] = active_channels[c].loop.midi_events[e].status; + msg[1] = active_channels[c].loop.midi_events[e].note; + msg[2] = active_channels[c].loop.midi_events[e].velocity; + jack_midi_event_write(midi_out_buf, 0, msg, 3); + } + } } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + break; } - 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); + case STATE_PAUSED: + /* no output */ + break; + default: /* IDLE */ + /* pass through MIDI input to output */ + { + 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; + } + /* for MIDI channels, the loop_count holds number of recorded events */ + if (state == STATE_LOOPING) { + active_channels[c].loop_count = active_channels[c].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++) { + if (active_channels[c].record_pos < LOOP_BUF_SIZE) + active_channels[c].loop.audio_buffer[active_channels[c].record_pos++] = + f_in[i]; + f_out[i] = f_in[i]; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); + } + break; + + case STATE_LOOPING: + if (active_channels[c].loop_count > 0) { + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + outf[i] = + active_channels[c].loop.audio_buffer[active_channels[c].playback_pos]; + active_channels[c].playback_pos = + (active_channels[c].playback_pos + 1) % + active_channels[c].loop_count; + } + } 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; } - break; } active_channels[c].prev_state = state; @@ -341,6 +416,20 @@ void looper_process_commands(jack_client_t *client) { channel_add(client, idx); break; } + case CMD_ADD_MIDI_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int idx; + for (idx = 0; idx < cap; idx++) + if (!atomic_load(&cur[idx].active)) + break; + if (idx == cap) { + if (ensure_capacity(client, idx) != 0) + break; + } + channel_add_midi(client, idx); + break; + } case CMD_REMOVE_CHANNEL: { int cap = atomic_load(&channel_capacity); struct channel_t *cur = get_channels_array(); @@ -375,6 +464,20 @@ void looper_process_commands(jack_client_t *client) { channel_add(client, idx); break; } + case CMD_ADD_MIDI_CHANNEL: { + int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); + int idx; + for (idx = 0; idx < cap; idx++) + if (!atomic_load(&cur[idx].active)) + break; + if (idx == cap) { + if (ensure_capacity(client, idx) != 0) + break; + } + channel_add_midi(client, idx); + break; + } case CMD_REMOVE_CHANNEL: { int cap = atomic_load(&channel_capacity); struct channel_t *cur = get_channels_array();