2-midi-looping #3

Merged
boomjacky merged 17 commits from 2-midi-looping into master 2026-05-10 12:24:23 -04:00
4 changed files with 199 additions and 42 deletions
Showing only changes of commit ff226a8ea6 - Show all commits

View File

@@ -29,6 +29,37 @@ void channel_add(jack_client_t *client, int idx) {
cur[idx].loop_count = 0; cur[idx].loop_count = 0;
cur[idx].record_pos = 0; cur[idx].record_pos = 0;
cur[idx].playback_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++; next_channel_id++;
atomic_fetch_add(&channel_count, 1); atomic_fetch_add(&channel_count, 1);

View File

@@ -7,6 +7,20 @@
#define LOOP_BUF_SIZE (5 * 48000) #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 { typedef enum {
STATE_IDLE, STATE_IDLE,
STATE_RECORD, STATE_RECORD,
@@ -15,15 +29,22 @@ typedef enum {
} looper_state; } looper_state;
struct channel_t { struct channel_t {
channel_type_t type; /* CHANNEL_AUDIO or CHANNEL_MIDI */
atomic_int state; atomic_int state;
int prev_state; int prev_state;
float loop_buffer[LOOP_BUF_SIZE]; union {
int loop_count; float audio_buffer[LOOP_BUF_SIZE];
int record_pos; midi_event_t midi_events[MAX_MIDI_EVENTS];
int playback_pos; } 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; atomic_int active;
jack_port_t *audio_in; jack_port_t *audio_in;
jack_port_t *audio_out; jack_port_t *audio_out;
jack_port_t *midi_in;
jack_port_t *midi_out;
}; };
/* Globals declared in looper.c */ /* 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_add(jack_client_t *client, int idx);
void channel_remove(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 #endif

View File

@@ -8,6 +8,7 @@ typedef enum {
CMD_UNBIND, // reset bind to channel 0 CMD_UNBIND, // reset bind to channel 0
CMD_ADD_CHANNEL, // add a new dynamic channel CMD_ADD_CHANNEL, // add a new dynamic channel
CMD_REMOVE_CHANNEL, // remove last dynamic channel CMD_REMOVE_CHANNEL, // remove last dynamic channel
CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel
} cmd_type_t; } cmd_type_t;
typedef struct { typedef struct {

View File

@@ -175,49 +175,124 @@ int process_callback(jack_nframes_t nframes, void *arg) {
} }
} }
jack_nframes_t i; if (active_channels[c].type == CHANNEL_MIDI) {
switch (state) { /* MIDI channel handling */
case STATE_RECORD: switch (state) {
if (in) { case STATE_RECORD: {
float *f_out = (float *)out; void *midi_in_buf = jack_port_get_buffer(active_channels[c].midi_in, nframes);
const float *f_in = (const float *)in; if (midi_in_buf) {
for (i = 0; i < nframes; i++) { jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf);
if (active_channels[c].record_pos < LOOP_BUF_SIZE) jack_midi_event_t ev;
active_channels[c].loop_buffer[active_channels[c].record_pos++] = for (jack_nframes_t j = 0; j < nevents; j++) {
f_in[i]; if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) continue;
f_out[i] = f_in[i]; 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 { break;
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
} }
break; case STATE_LOOPING: {
void *midi_out_buf = jack_port_get_buffer(active_channels[c].midi_out, nframes);
case STATE_LOOPING: if (midi_out_buf) {
if (active_channels[c].loop_count > 0) { jack_midi_clear_buffer(midi_out_buf);
float *outf = (float *)out; int cnt = active_channels[c].loop_count; /* number of recorded events */
for (i = 0; i < nframes; i++) { if (cnt > 0) {
outf[i] = /* simple: output all recorded events at frame 0 of each cycle */
active_channels[c].loop_buffer[active_channels[c].playback_pos]; for (int e = 0; e < cnt; e++) {
active_channels[c].playback_pos = unsigned char msg[3];
(active_channels[c].playback_pos + 1) % msg[0] = active_channels[c].loop.midi_events[e].status;
active_channels[c].loop_count; 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 { break;
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
} }
break; case STATE_PAUSED:
/* no output */
case STATE_PAUSED: break;
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); default: /* IDLE */
break; /* pass through MIDI input to output */
{
default: /* IDLE */ void *midi_in_buf = jack_port_get_buffer(active_channels[c].midi_in, nframes);
if (in) { void *midi_out_buf = jack_port_get_buffer(active_channels[c].midi_out, nframes);
memcpy(out, in, sizeof(jack_default_audio_sample_t) * nframes); if (midi_in_buf && midi_out_buf) {
} else { jack_midi_clear_buffer(midi_out_buf);
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); 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; active_channels[c].prev_state = state;
@@ -341,6 +416,20 @@ void looper_process_commands(jack_client_t *client) {
channel_add(client, idx); channel_add(client, idx);
break; 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: { case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity); int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array(); struct channel_t *cur = get_channels_array();
@@ -375,6 +464,20 @@ void looper_process_commands(jack_client_t *client) {
channel_add(client, idx); channel_add(client, idx);
break; 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: { case CMD_REMOVE_CHANNEL: {
int cap = atomic_load(&channel_capacity); int cap = atomic_load(&channel_capacity);
struct channel_t *cur = get_channels_array(); struct channel_t *cur = get_channels_array();