feat: add per-channel MIDI looping support
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
107
src/looper.c
107
src/looper.c
@@ -175,6 +175,80 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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:
|
||||
@@ -183,7 +257,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
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++] =
|
||||
active_channels[c].loop.audio_buffer[active_channels[c].record_pos++] =
|
||||
f_in[i];
|
||||
f_out[i] = f_in[i];
|
||||
}
|
||||
@@ -197,7 +271,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
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].loop.audio_buffer[active_channels[c].playback_pos];
|
||||
active_channels[c].playback_pos =
|
||||
(active_channels[c].playback_pos + 1) %
|
||||
active_channels[c].loop_count;
|
||||
@@ -219,6 +293,7 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
||||
}
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user