diff --git a/src/main.c b/src/main.c index 5e5a2de..a605bdd 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include #define LOOP_BUF_SIZE (5 * 48000) /* 5 seconds at 48 kHz, mono */ +#define MAX_CHANNELS 16 typedef enum { STATE_IDLE, @@ -16,151 +17,187 @@ typedef enum { STATE_PAUSED } looper_state; -static atomic_int current_state = STATE_IDLE; +/* per‑channel state */ +struct channel_t { + atomic_int state; + int prev_state; + float loop_buffer[LOOP_BUF_SIZE]; + int loop_count; + int record_pos; + int playback_pos; + int active; /* 1 = channel in use */ + jack_port_t *audio_in; + jack_port_t *audio_out; +}; -/* loop buffer and playback state */ -static float loop_buffer[LOOP_BUF_SIZE]; -static int loop_count = 0; /* number of recorded samples */ -static int record_pos = 0; /* next write index while recording */ -static int playback_pos = 0; /* next read index while looping */ -static int prev_state = -1; /* for detecting state changes */ +static struct channel_t channels[MAX_CHANNELS]; +static atomic_int channel_count = 0; /* number of active channels */ +static int next_channel_id = 1; /* for port naming */ +static atomic_int cmd_add = 0; /* set by MIDI note 60 in process() */ +static atomic_int cmd_remove = 0; /* set by MIDI note 61 */ -static jack_port_t *input_port; -static jack_port_t *output_port; static jack_port_t *midi_control_port; static jack_port_t *midi_clock_port; - static jack_client_t *client; +/* --------------------------------------------------------------- + * process callback – runs in real‑time context + * --------------------------------------------------------------- */ static int process(jack_nframes_t nframes, void *arg) { (void)arg; - jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) jack_port_get_buffer(input_port, nframes); - jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) jack_port_get_buffer(output_port, nframes); - /* ----- state change detection ----- */ - int state = atomic_load(¤t_state); - if (state != prev_state) { - if (state == STATE_RECORD) { - record_pos = 0; - loop_count = 0; - } else if (state == STATE_LOOPING) { - if (record_pos > 0) { - loop_count = record_pos; /* what we recorded */ - } - playback_pos = 0; /* restart from beginning */ - } - } - - /* ----- handle MIDI control port (state transitions) ----- */ + /* handle MIDI commands on the global control port */ void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); if (midi_ctrl_buf) { jack_nframes_t nevents = jack_midi_get_event_count(midi_ctrl_buf); jack_midi_event_t ev; for (jack_nframes_t i = 0; i < nevents; i++) { - if (jack_midi_event_get(&ev, midi_ctrl_buf, i) == 0) { - /* note on with note number 1 */ - if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) { - unsigned char note = ev.buffer[1]; - if (note == 1) { - int cur_state = atomic_load(¤t_state); - switch (cur_state) { - case STATE_IDLE: - atomic_store(¤t_state, STATE_RECORD); - break; - case STATE_RECORD: - atomic_store(¤t_state, STATE_LOOPING); - break; - case STATE_LOOPING: - atomic_store(¤t_state, STATE_PAUSED); - break; - case STATE_PAUSED: - atomic_store(¤t_state, STATE_LOOPING); - break; - } + if (jack_midi_event_get(&ev, midi_ctrl_buf, i) != 0) continue; + if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) { + unsigned char note = ev.buffer[1]; + switch (note) { + case 1: /* toggle state of channel 0 (backward compatible) */ + { + int cur0 = atomic_load(&channels[0].state); + switch (cur0) { + case STATE_IDLE: + atomic_store(&channels[0].state, STATE_RECORD); + break; + case STATE_RECORD: + atomic_store(&channels[0].state, STATE_LOOPING); + break; + case STATE_LOOPING: + atomic_store(&channels[0].state, STATE_PAUSED); + break; + case STATE_PAUSED: + atomic_store(&channels[0].state, STATE_LOOPING); + break; } + break; + } + case 60: /* add channel – send to main thread */ + atomic_store(&cmd_add, 1); + break; + case 61: /* remove channel – send to main thread */ + atomic_store(&cmd_remove, 1); + break; + default: + break; } } } } - /* ----- audio output based on current state ----- */ - if (!out) return 0; /* cannot happen, but safe */ + /* process each active channel */ + for (int c = 0; c < MAX_CHANNELS; c++) { + if (!channels[c].active) continue; - jack_nframes_t i; + jack_default_audio_sample_t *in = (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; /* safety */ - switch (state) { - case STATE_RECORD: - if (in) { - const float *inf = (const float *)in; - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - if (record_pos < LOOP_BUF_SIZE) { - loop_buffer[record_pos] = inf[i]; - record_pos++; + int state = atomic_load(&channels[c].state); + + /* transition initialisation */ + if (state != channels[c].prev_state) { + switch (state) { + case STATE_RECORD: + channels[c].record_pos = 0; + channels[c].loop_count = 0; + break; + case STATE_LOOPING: + if (channels[c].record_pos > 0) + channels[c].loop_count = channels[c].record_pos; + channels[c].playback_pos = 0; + break; + default: + break; + } + } + + jack_nframes_t i; + + switch (state) { + case STATE_RECORD: + if (in) { + const float *inf = (const float *)in; + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + if (channels[c].record_pos < LOOP_BUF_SIZE) { + channels[c].loop_buffer[channels[c].record_pos] = inf[i]; + channels[c].record_pos++; + } + outf[i] = inf[i]; /* monitor input */ } - outf[i] = inf[i]; /* monitor input */ + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } - } else { - memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); - } - break; + break; - case STATE_LOOPING: - if (loop_count > 0) { - float *outf = (float *)out; - for (i = 0; i < nframes; i++) { - outf[i] = loop_buffer[playback_pos]; - playback_pos = (playback_pos + 1) % loop_count; + case STATE_LOOPING: + if (channels[c].loop_count > 0) { + float *outf = (float *)out; + for (i = 0; i < nframes; i++) { + outf[i] = channels[c].loop_buffer[channels[c].playback_pos]; + channels[c].playback_pos = + (channels[c].playback_pos + 1) % channels[c].loop_count; + } + } else { + memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes); } - } 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 { + 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; + + channels[c].prev_state = state; } - /* ----- MIDI clock events (unchanged) ----- */ + /* ----- MIDI clock events (still affect channel 0 only) ----- */ 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) { - if (cev.size >= 1) { - unsigned char msg = cev.buffer[0]; - if (msg == 0xFA) { - int s = atomic_load(¤t_state); - if (s == STATE_IDLE) { - atomic_store(¤t_state, STATE_RECORD); - } - } else if (msg == 0xFC) { - atomic_store(¤t_state, STATE_IDLE); - } else if (msg == 0xFB) { - int s = atomic_load(¤t_state); - if (s == STATE_PAUSED) { - atomic_store(¤t_state, STATE_LOOPING); - } - } + 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; } } } } - /* update prev_state after all state changes */ - prev_state = atomic_load(¤t_state); - return 0; } @@ -182,35 +219,46 @@ int main(int argc, char *argv[]) client = jack_client_open(client_name, options, &status); if (client == NULL) { fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); - if (status & JackServerFailed) { + if (status & JackServerFailed) fprintf(stderr, "Unable to connect to JACK server\n"); - } return 1; } - if (status & JackNameNotUnique) { + if (status & JackNameNotUnique) client_name = jack_get_client_name(client); - } jack_set_process_callback(client, process, NULL); jack_on_shutdown(client, jack_shutdown, NULL); - input_port = jack_port_register(client, "input", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsInput, 0); - output_port = jack_port_register(client, "output", - JACK_DEFAULT_AUDIO_TYPE, - JackPortIsOutput, 0); + /* ------------------ channel 0 (the default channel) ------------------ */ + channels[0].active = 1; + atomic_store(&channels[0].state, STATE_IDLE); + channels[0].prev_state = -1; + channels[0].loop_count = 0; + channels[0].record_pos = 0; + channels[0].playback_pos = 0; + + 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 & clock ports (shared across all channels) */ 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 ((input_port == NULL) || (output_port == NULL) || - (midi_control_port == NULL) || (midi_clock_port == NULL)) { - fprintf(stderr, "Could not create ports\n"); + 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; } @@ -221,10 +269,56 @@ int main(int argc, char *argv[]) fprintf(stderr, "looper running (client name '%s')\n", client_name); - - prev_state = -1; /* initialise change detection */ - while (1) { + /* process pending add‑channel / remove‑channel commands + * (only safe outside the real‑time callback) */ + if (atomic_exchange(&cmd_add, 0)) { + int idx; + for (idx = 0; idx < MAX_CHANNELS; idx++) + if (!channels[idx].active) break; + if (idx < MAX_CHANNELS) { + channels[idx].active = 1; + atomic_store(&channels[idx].state, STATE_IDLE); + channels[idx].prev_state = -1; + channels[idx].loop_count = 0; + channels[idx].record_pos = 0; + channels[idx].playback_pos = 0; + + char in_name[64], out_name[64]; + snprintf(in_name, sizeof(in_name), + "channel%d_input", next_channel_id); + snprintf(out_name, sizeof(out_name), + "channel%d_output", next_channel_id); + + channels[idx].audio_in = + jack_port_register(client, in_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + channels[idx].audio_out = + jack_port_register(client, out_name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + if (!channels[idx].audio_in || !channels[idx].audio_out) + fprintf(stderr, "Failed to register ports for channel %d\n", + next_channel_id); + next_channel_id++; + channel_count++; + } + } + + if (atomic_exchange(&cmd_remove, 0)) { + /* find the highest active channel index >0 */ + int remove_idx = -1; + for (int idx = 1; idx < MAX_CHANNELS; idx++) + if (channels[idx].active) remove_idx = idx; + if (remove_idx != -1) { + jack_port_unregister(client, channels[remove_idx].audio_in); + jack_port_unregister(client, channels[remove_idx].audio_out); + channels[remove_idx].active = 0; + channel_count--; + } + } + sleep(1); }