1-multichannel #1

Merged
boomjacky merged 31 commits from 1-multichannel into multichannel 2026-05-09 15:47:09 -04:00
2 changed files with 230 additions and 228 deletions
Showing only changes of commit 1db9735e1b - Show all commits

View File

@@ -1,24 +1,24 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdatomic.h>
#include <math.h>
#include "looper.h" #include "looper.h"
#include "channel.h" #include "channel.h"
#include "midi.h" #include "midi.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Global state (shared across files) */ /* Global state (shared across files) */
struct channel_t channels[MAX_CHANNELS]; struct channel_t channels[MAX_CHANNELS];
atomic_int channel_count = 0; atomic_int channel_count = 0;
int next_channel_id = 1; int next_channel_id = 1;
atomic_int cmd_add = 0; atomic_int cmd_add = 0;
atomic_int cmd_remove = 0; atomic_int cmd_remove = 0;
jack_port_t *midi_control_port = NULL; jack_port_t *midi_control_port = NULL;
jack_port_t *midi_clock_port = NULL; jack_port_t *midi_clock_port = NULL;
atomic_int control_key_active = 0; atomic_int control_key_active = 0;
atomic_int bind_channel = 0; atomic_int bind_channel = 0;
/* Deferred removal index (1 second grace) */ /* Deferred removal index (1 second grace) */
static int pending_unregister_idx = -1; static int pending_unregister_idx = -1;
@@ -26,211 +26,214 @@ static int pending_unregister_idx = -1;
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
* process callback * process callback
* ---------------------------------------------------------------- */ * ---------------------------------------------------------------- */
int process_callback(jack_nframes_t nframes, void *arg) int process_callback(jack_nframes_t nframes, void *arg) {
{ (void)arg;
(void)arg;
if (midi_control_port) { if (midi_control_port) {
void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes); void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes);
if (midi_ctrl_buf) { if (midi_ctrl_buf) {
midi_handle_events(midi_ctrl_buf, nframes); 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;
} }
/* process each active channel */ jack_default_audio_sample_t *in =
for (int c = 0; c < MAX_CHANNELS; c++) { (jack_default_audio_sample_t *)jack_port_get_buffer(
if (!atomic_load(&channels[c].active)) continue; 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;
/* Guard against NULL ports (e.g. if port registration failed) */ int state = atomic_load(&channels[c].state);
if (!channels[c].audio_in || !channels[c].audio_out) {
fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c);
continue;
}
jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) if (state != channels[c].prev_state) {
jack_port_get_buffer(channels[c].audio_in, nframes); switch (state) {
jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) case STATE_RECORD:
jack_port_get_buffer(channels[c].audio_out, nframes); channels[c].record_pos = 0;
if (!out) continue; channels[c].loop_count = 0;
break;
int state = atomic_load(&channels[c].state); case STATE_LOOPING:
if (channels[c].record_pos > 0)
if (state != channels[c].prev_state) { channels[c].loop_count = channels[c].record_pos;
switch (state) { channels[c].playback_pos = 0;
case STATE_RECORD: break;
channels[c].record_pos = 0; default:
channels[c].loop_count = 0; break;
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) {
for (i = 0; i < nframes; i++) {
if (channels[c].record_pos < LOOP_BUF_SIZE)
channels[c].loop_buffer[channels[c].record_pos++] = ((const float *)in)[i];
((float *)out)[i] = ((const float *)in)[i];
}
} else {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
}
break;
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);
}
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;
}
channels[c].prev_state = state;
} }
/* MIDI clock events affect channel 0 only */ jack_nframes_t i;
if (midi_clock_port) { switch (state) {
void *midi_clock_buf = jack_port_get_buffer(midi_clock_port, nframes); case STATE_RECORD:
if (midi_clock_buf) { if (in) {
jack_nframes_t n_clock_events = jack_midi_get_event_count(midi_clock_buf); for (i = 0; i < nframes; i++) {
jack_midi_event_t cev; if (channels[c].record_pos < LOOP_BUF_SIZE)
for (jack_nframes_t j = 0; j < n_clock_events; j++) { channels[c].loop_buffer[channels[c].record_pos++] =
if (jack_midi_event_get(&cev, midi_clock_buf, j) != 0) continue; ((const float *)in)[i];
if (cev.size >= 1) { ((float *)out)[i] = ((const float *)in)[i];
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;
}
}
}
} }
} else {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
}
break;
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);
}
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;
} }
return 0; 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 * shutdown callback
* ---------------------------------------------------------------- */ * ---------------------------------------------------------------- */
void jack_shutdown_cb(void *arg) void jack_shutdown_cb(void *arg) {
{ (void)arg;
(void)arg; fprintf(stderr, "JACK shutdown\n");
fprintf(stderr, "JACK shutdown\n"); exit(0);
exit(0);
} }
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
* looper initialisation * looper initialisation
* ---------------------------------------------------------------- */ * ---------------------------------------------------------------- */
int looper_init(jack_client_t *client) int looper_init(jack_client_t *client) {
{ /* channel 0 */
/* channel 0 */ channels[0].active = 1;
channels[0].active = 1; atomic_store(&channels[0].state, STATE_IDLE);
atomic_store(&channels[0].state, STATE_IDLE); channels[0].prev_state = -1;
channels[0].prev_state = -1; channels[0].loop_count = 0;
channels[0].loop_count = 0; channels[0].record_pos = 0;
channels[0].record_pos = 0; channels[0].playback_pos = 0;
channels[0].playback_pos = 0;
channels[0].audio_in = jack_port_register(client, "input", channels[0].audio_in = jack_port_register(
JACK_DEFAULT_AUDIO_TYPE, client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
JackPortIsInput, 0); channels[0].audio_out = jack_port_register(
channels[0].audio_out = jack_port_register(client, "output", client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
JACK_DEFAULT_AUDIO_TYPE, if (!channels[0].audio_in || !channels[0].audio_out) {
JackPortIsOutput, 0); fprintf(stderr, "Could not create audio ports for channel 0\n");
if (!channels[0].audio_in || !channels[0].audio_out) { return -1;
fprintf(stderr, "Could not create audio ports for channel 0\n"); }
return -1; channel_count = 1;
}
channel_count = 1;
midi_control_port = jack_port_register(client, "control", midi_control_port = jack_port_register(
JACK_DEFAULT_MIDI_TYPE, client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
JackPortIsInput, 0); midi_clock_port = jack_port_register(client, "clock", JACK_DEFAULT_MIDI_TYPE,
midi_clock_port = jack_port_register(client, "clock", JackPortIsInput, 0);
JACK_DEFAULT_MIDI_TYPE, if (!midi_control_port || !midi_clock_port) {
JackPortIsInput, 0); fprintf(stderr, "Could not create MIDI ports\n");
if (!midi_control_port || !midi_clock_port) { return -1;
fprintf(stderr, "Could not create MIDI ports\n"); }
return -1;
}
return 0; return 0;
} }
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
* mainloop command processing * mainloop command processing
* ---------------------------------------------------------------- */ * ---------------------------------------------------------------- */
void looper_process_commands(jack_client_t *client) void looper_process_commands(jack_client_t *client) {
{ /* Unregister any ports that were marked for deferred removal.
/* Unregister any ports that were marked for deferred removal. By now the realtime thread has had at least one full cycle
By now the realtime thread has had at least one full cycle to see the `active = 0` store. */
to see the `active = 0` store. */ if (pending_unregister_idx != -1) {
if (pending_unregister_idx != -1) { int idx = pending_unregister_idx;
int idx = pending_unregister_idx; if (channels[idx].audio_in)
if (channels[idx].audio_in) jack_port_unregister(client, channels[idx].audio_in);
jack_port_unregister(client, channels[idx].audio_in); if (channels[idx].audio_out)
if (channels[idx].audio_out) jack_port_unregister(client, channels[idx].audio_out);
jack_port_unregister(client, channels[idx].audio_out); pending_unregister_idx = -1;
pending_unregister_idx = -1; }
}
if (atomic_exchange(&cmd_add, 0)) { if (atomic_exchange(&cmd_add, 0)) {
int idx; int idx;
for (idx = 0; idx < MAX_CHANNELS; idx++) for (idx = 0; idx < MAX_CHANNELS; idx++)
if (!channels[idx].active) break; if (!channels[idx].active)
if (idx < MAX_CHANNELS) { break;
channel_add(client, idx); if (idx < MAX_CHANNELS) {
} channel_add(client, idx);
} }
}
if (atomic_exchange(&cmd_remove, 0)) { if (atomic_exchange(&cmd_remove, 0)) {
int remove_idx = -1; int remove_idx = -1;
for (int idx = 1; idx < MAX_CHANNELS; idx++) for (int idx = 1; idx < MAX_CHANNELS; idx++)
if (channels[idx].active) remove_idx = idx; if (channels[idx].active)
if (remove_idx != -1) { remove_idx = idx;
/* Mark inactive now; ports will be unregistered next round */ if (remove_idx != -1) {
channel_remove(client, remove_idx); /* Mark inactive now; ports will be unregistered next round */
pending_unregister_idx = remove_idx; channel_remove(client, remove_idx);
} pending_unregister_idx = remove_idx;
} }
}
} }

View File

@@ -1,50 +1,49 @@
#include "looper.h"
#include <jack/jack.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <jack/jack.h>
#include "looper.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[]) {
{ (void)argc;
(void)argc; (void)argv;
(void)argv; const char *client_name = "looper";
const char *client_name = "looper"; jack_options_t options = JackNullOption;
jack_options_t options = JackNullOption; jack_status_t status;
jack_status_t status;
jack_client_t *client = jack_client_open(client_name, options, &status); jack_client_t *client = jack_client_open(client_name, options, &status);
if (client == NULL) { if (client == NULL) {
fprintf(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status); 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"); fprintf(stderr, "Unable to connect to JACK server\n");
return 1; return 1;
} }
if (status & JackNameNotUnique) if (status & JackNameNotUnique)
client_name = jack_get_client_name(client); client_name = jack_get_client_name(client);
jack_set_process_callback(client, process_callback, NULL); jack_set_process_callback(client, process_callback, NULL);
jack_on_shutdown(client, jack_shutdown_cb, NULL); jack_on_shutdown(client, jack_shutdown_cb, NULL);
if (looper_init(client) != 0) {
fprintf(stderr, "looper initialisation failed\n");
jack_client_close(client);
return 1;
}
if (jack_activate(client)) {
fprintf(stderr, "Cannot activate client\n");
jack_client_close(client);
return 1;
}
fprintf(stderr, "looper running (client name '%s')\n", client_name);
while (1) {
looper_process_commands(client);
usleep(50000); /* check commands every 50 ms */
}
if (looper_init(client) != 0) {
fprintf(stderr, "looper initialisation failed\n");
jack_client_close(client); jack_client_close(client);
return 0; return 1;
}
if (jack_activate(client)) {
fprintf(stderr, "Cannot activate client\n");
jack_client_close(client);
return 1;
}
fprintf(stderr, "looper running (client name '%s')\n", client_name);
while (1) {
looper_process_commands(client);
usleep(50000); /* check commands every 50 ms */
}
jack_client_close(client);
return 0;
} }