Merge branch 'multichannel'

This commit is contained in:
Loic Coenen
2026-05-09 19:54:08 +00:00
12 changed files with 514 additions and 210 deletions

24
evaluation.md Normal file
View File

@@ -0,0 +1,24 @@
# Code Evaluation
## Summary Table
| Category | Rating | Remarks |
|--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Mocked / Left Undone | ✅ OK | Multichannel and dynamic channel add/remove are now implemented. Control key (note64) is handled as a modifier for command selection. Backward compatibility for note1,60,61 retained. |
| Potential Segfaults | ✅ Fixed | Added null checks for both `audio_in` and `audio_out` in the process callback, and `channel_add` no longer marks the channel active if port registration fails. |
| Memory Safety | ✅ OK | No dynamic memory allocation; only a fixedsize global buffer. No leaks, no useafterfree. |
| Thread Safety / Race | ⚠️ Warning | `atomic_load`/`store` on `current_state` is correct, but the audio processing uses the *original* state loaded *before* MIDI events are handled in the same callback. State changes that occur in the current cycle are ignored until the next cycle can cause missed transitions (e.g., start recording one cycle late). |
| Performance | ✅ OK | Linear buffer access, no system calls or allocations in the realtime callback. Atomic operations are cheap. Fixed buffer size (0.96 MB) is safe. |
| Architectural Soundness | ✅ OK | Dynamic multichannel architecture with perchannel state and ports. Realtime safe command queue via atomic flags. Abstraction via `channel_t` struct. Extensible for future binding. |
## Test Evaluation
| Aspect | Remarks |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `test_audio_pass_through` | Verifies basic audio connectivity; passes when JACK server running. Does not test any looperspecific behavior beyond passthrough. |
| `test_looper_looping` | Exercises the state machine (IDLE→RECORD→LOOPING) using MIDI note 1. Detects repeated audio bursts. Works with current implementation but uses note 1 instead of the required control key (64). The 0.1second beep and 4second wait may be sensitive to CPU load. |
| `test_multiple_channels` | Expects dynamic channel creation via note 60 (add channel). Current looper does not handle this command, causing immediate failure. This test is effectively a placeholder for future implementation. |
| Coverage gaps | No tests for: control key note 64, remove channel, binding, perchannel loops, state transitions other than note 1, robust handling of JACK server disconnection. |
| Thread safety | The test assumes sequential execution and uses long sleeps for synchronization. The realtime thread is managed by JACK; the test process runs asynchronously, which can lead to timingsensitive failures on heavily loaded systems. |
| Resource handling | Tests properly kill child process and close JACK clients. No memory leaks. |
| Overall verdict | The test suite provides a minimal smokecheck but does **not** validate the full specification. It must be updated to use the correct control key (64), cover dynamic channel commands (add/remove/bind), and handle nonexistent features before it can be considered a trustworthy integration test. |

View File

Binary file not shown.

BIN
looper
View File

Binary file not shown.

View File

@@ -1,9 +1,15 @@
CC ?= gcc CC ?= gcc
CFLAGS ?= -Wall -Wextra -g CFLAGS ?= -Wall -Wextra -g -Isrc
LDFLAGS ?= -ljack LDFLAGS ?= -ljack -lm
looper: src/main.c SRC = src/main.c src/looper.c src/channel.c src/midi.c
$(CC) $(CFLAGS) -o looper src/main.c $(LDFLAGS) OBJ = $(SRC:.c=.o)
looper: $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
src/%.o: src/%.c
$(CC) $(CFLAGS) -c -o $@ $<
integration: looper tests/integration.c integration: looper tests/integration.c
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm $(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm
@@ -13,4 +19,4 @@ test: integration
.PHONY: clean integration test .PHONY: clean integration test
clean: clean:
rm -f looper integration_test rm -f looper integration_test src/*.o

43
src/channel.c Normal file
View File

@@ -0,0 +1,43 @@
#include <stdio.h>
#include <string.h>
#include <jack/jack.h>
#include <stdatomic.h>
#include "channel.h"
void channel_add(jack_client_t *client, int idx)
{
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);
/* Do NOT mark channel active process loop will skip it */
channels[idx].active = 0;
return;
}
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;
next_channel_id++;
channel_count++;
}
void channel_remove(jack_client_t *client, int idx)
{
jack_port_unregister(client, channels[idx].audio_in);
jack_port_unregister(client, channels[idx].audio_out);
channels[idx].active = 0;
channel_count--;
}

39
src/channel.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef CHANNEL_H
#define CHANNEL_H
#include <jack/jack.h>
#include <stdatomic.h>
#define LOOP_BUF_SIZE (5 * 48000)
#define MAX_CHANNELS 16
typedef enum {
STATE_IDLE,
STATE_RECORD,
STATE_LOOPING,
STATE_PAUSED
} looper_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;
jack_port_t *audio_in;
jack_port_t *audio_out;
};
/* Globals declared in looper.c */
extern struct channel_t channels[MAX_CHANNELS];
extern atomic_int channel_count;
extern int next_channel_id;
extern atomic_int cmd_add;
extern atomic_int cmd_remove;
void channel_add(jack_client_t *client, int idx);
void channel_remove(jack_client_t *client, int idx);
#endif

214
src/looper.c Normal file
View File

@@ -0,0 +1,214 @@
#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 "channel.h"
#include "midi.h"
/* Global state (shared across files) */
struct channel_t channels[MAX_CHANNELS];
atomic_int channel_count = 0;
int next_channel_id = 1;
atomic_int cmd_add = 0;
atomic_int cmd_remove = 0;
jack_port_t *midi_control_port = NULL;
jack_port_t *midi_clock_port = NULL;
atomic_int control_key_active = 0;
/* ----------------------------------------------------------------
* process callback
* ---------------------------------------------------------------- */
int process_callback(jack_nframes_t nframes, void *arg)
{
(void)arg;
void *midi_ctrl_buf = jack_port_get_buffer(midi_control_port, nframes);
if (midi_ctrl_buf) {
midi_handle_events(midi_ctrl_buf, nframes);
}
/* process each active channel */
for (int c = 0; c < MAX_CHANNELS; c++) {
if (!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;
}
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;
int state = atomic_load(&channels[c].state);
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) {
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 */
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
* ---------------------------------------------------------------- */
void jack_shutdown_cb(void *arg)
{
(void)arg;
fprintf(stderr, "JACK shutdown\n");
exit(0);
}
/* ----------------------------------------------------------------
* looper initialisation
* ---------------------------------------------------------------- */
int looper_init(jack_client_t *client)
{
/* channel 0 */
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_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 (!midi_control_port || !midi_clock_port) {
fprintf(stderr, "Could not create MIDI ports\n");
return -1;
}
return 0;
}
/* ----------------------------------------------------------------
* mainloop command processing
* ---------------------------------------------------------------- */
void looper_process_commands(jack_client_t *client)
{
if (atomic_exchange(&cmd_add, 0)) {
int idx;
for (idx = 0; idx < MAX_CHANNELS; idx++)
if (!channels[idx].active) break;
if (idx < MAX_CHANNELS) {
channel_add(client, idx);
}
}
if (atomic_exchange(&cmd_remove, 0)) {
int remove_idx = -1;
for (int idx = 1; idx < MAX_CHANNELS; idx++)
if (channels[idx].active) remove_idx = idx;
if (remove_idx != -1) {
channel_remove(client, remove_idx);
}
}
}

18
src/looper.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef LOOPER_H
#define LOOPER_H
#include <jack/jack.h>
/* Initialisation must be called after setting process callback */
int looper_init(jack_client_t *client);
/* Process callback to be called by JACK */
int process_callback(jack_nframes_t nframes, void *arg);
/* Shutdown callback */
void jack_shutdown_cb(void *arg);
/* Mainloop command processing (add/remove channels) */
void looper_process_commands(jack_client_t *client);
#endif

View File

@@ -1,183 +1,8 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <string.h>
#include <signal.h>
#include <jack/jack.h> #include <jack/jack.h>
#include <jack/midiport.h> #include "looper.h"
#include <stdatomic.h>
#include <math.h>
#define LOOP_BUF_SIZE (5 * 48000) /* 5 seconds at 48 kHz, mono */
typedef enum {
STATE_IDLE,
STATE_RECORD,
STATE_LOOPING,
STATE_PAUSED
} looper_state;
static atomic_int current_state = STATE_IDLE;
/* 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 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;
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(&current_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) ----- */
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(&current_state);
switch (cur_state) {
case STATE_IDLE:
atomic_store(&current_state, STATE_RECORD);
break;
case STATE_RECORD:
atomic_store(&current_state, STATE_LOOPING);
break;
case STATE_LOOPING:
atomic_store(&current_state, STATE_PAUSED);
break;
case STATE_PAUSED:
atomic_store(&current_state, STATE_LOOPING);
break;
}
}
}
}
}
}
/* ----- audio output based on current state ----- */
if (!out) return 0; /* cannot happen, but safe */
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 (record_pos < LOOP_BUF_SIZE) {
loop_buffer[record_pos] = inf[i];
record_pos++;
}
outf[i] = inf[i]; /* monitor input */
}
} else {
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
}
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;
}
} 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;
}
/* ----- MIDI clock events (unchanged) ----- */
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(&current_state);
if (s == STATE_IDLE) {
atomic_store(&current_state, STATE_RECORD);
}
} else if (msg == 0xFC) {
atomic_store(&current_state, STATE_IDLE);
} else if (msg == 0xFB) {
int s = atomic_load(&current_state);
if (s == STATE_PAUSED) {
atomic_store(&current_state, STATE_LOOPING);
}
}
}
}
}
}
/* update prev_state after all state changes */
prev_state = atomic_load(&current_state);
return 0;
}
static void jack_shutdown(void *arg)
{
(void)arg;
fprintf(stderr, "JACK shutdown\n");
exit(0);
}
static void sigusr1_handler(int signo) {
(void)signo;
int state = atomic_load(&current_state);
int code = state + 1;
_exit(code);
}
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@@ -187,55 +12,37 @@ int main(int argc, char *argv[])
jack_options_t options = JackNullOption; jack_options_t options = JackNullOption;
jack_status_t status; jack_status_t status;
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, NULL); jack_set_process_callback(client, process_callback, NULL);
jack_on_shutdown(client, jack_shutdown, NULL); jack_on_shutdown(client, jack_shutdown_cb, NULL);
input_port = jack_port_register(client, "input", if (looper_init(client) != 0) {
JACK_DEFAULT_AUDIO_TYPE, fprintf(stderr, "looper initialisation failed\n");
JackPortIsInput, 0); jack_client_close(client);
output_port = jack_port_register(client, "output",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
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");
return 1; return 1;
} }
if (jack_activate(client)) { if (jack_activate(client)) {
fprintf(stderr, "Cannot activate client\n"); fprintf(stderr, "Cannot activate client\n");
jack_client_close(client);
return 1; return 1;
} }
fprintf(stderr, "looper running (client name '%s')\n", client_name); fprintf(stderr, "looper running (client name '%s')\n", client_name);
/* allow SIGUSR1 to report state and exit */
signal(SIGUSR1, sigusr1_handler);
prev_state = -1; /* initialise change detection */
while (1) { while (1) {
sleep(1); looper_process_commands(client);
usleep(50000); /* check commands every 50 ms */
} }
jack_client_close(client); jack_client_close(client);

91
src/midi.c Normal file
View File

@@ -0,0 +1,91 @@
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdatomic.h>
#include "midi.h"
#include "channel.h"
extern atomic_int control_key_active;
extern atomic_int cmd_add;
extern atomic_int cmd_remove;
void midi_handle_events(void *port_buffer, jack_nframes_t nframes)
{
(void)nframes;
jack_nframes_t nevents = jack_midi_get_event_count(port_buffer);
jack_midi_event_t ev;
for (jack_nframes_t i = 0; i < nevents; i++) {
if (jack_midi_event_get(&ev, port_buffer, i) != 0) continue;
if (ev.size < 3) continue;
unsigned char status = ev.buffer[0];
unsigned char note = ev.buffer[1];
unsigned char vel = ev.buffer[2];
/* noteon */
if ((status & 0xf0) == 0x90 && vel > 0) {
if (note == 64) {
atomic_store(&control_key_active, 1);
} else {
int ck = atomic_load(&control_key_active);
if (ck) {
atomic_store(&control_key_active, 0);
switch (note) {
case 60: atomic_store(&cmd_add, 1); break;
case 61: atomic_store(&cmd_remove, 1); break;
case 62: /* trigger looper channel 0 */
{
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;
default:
break;
}
} else {
/* direct mapping */
switch (note) {
case 1: /* toggle channel 0 */
{
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: atomic_store(&cmd_add, 1); break;
case 61: atomic_store(&cmd_remove, 1); break;
default:
break;
}
}
}
} else if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) {
atomic_store(&control_key_active, 0);
}
}
}

8
src/midi.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef MIDI_H
#define MIDI_H
#include <jack/types.h>
void midi_handle_events(void *port_buffer, jack_nframes_t nframes);
#endif

View File

@@ -366,6 +366,54 @@ static int test_looper_looping(void) {
return 0; return 0;
} }
/* test multiple channels */
static int test_multiple_channels(void) {
printf("Test: dynamic channel creation via MIDI command\n");
pid_t pid = start_looper();
if (pid < 0) return 1;
jack_client_t *client;
jack_status_t status;
client = jack_client_open("test_multi", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
if (send_jack_note_on("looper:control", 60, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send note 60 failed\n");
return 1;
}
/* wait long enough for the looper's main loop to process the add command
(it sleeps for 1 second between checks, so 1.5 s is safe) */
usleep(1500000);
int found = 0;
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
if (ports) {
for (int i = 0; ports[i]; i++) {
if (strstr(ports[i], "looper:channel1_input")) {
found = 1;
break;
}
}
jack_free(ports);
}
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
if (!found) {
fprintf(stderr, " FAIL: channel1_input port not created after add command\n");
return 1;
}
printf(" PASS (channel created)\n");
return 0;
}
int main(void) { int main(void) {
/* 1. binary must exist */ /* 1. binary must exist */
@@ -385,6 +433,12 @@ int main(void) {
return 1; return 1;
} }
/* 5. Test multiple dynamic channels */
if (test_multiple_channels() != 0) {
fprintf(stderr, " FAILED\n");
return 1;
}
printf("All tests completed successfully.\n"); printf("All tests completed successfully.\n");
return 0; return 0;
} }