Merge pull request '1-multichannel' (#1) from 1-multichannel into multichannel

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-09 15:47:08 -04:00
15 changed files with 1144 additions and 244 deletions

4
.githooks/pre-push Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
make test
make check
make format

72
docs/1-multichannel.md Normal file
View File

@@ -0,0 +1,72 @@
# MultiChannel & Bind Feature
The looper supports up to 16 independent channels (numbered 015).
Channel0 is always present and connected to the `looper:input` / `looper:output` audio ports.
Additional channels can be created and removed dynamically using MIDI commands.
## MIDI Ports
- **`looper:control`** receives MIDI noteon events for channel management and state toggling.
- **`looper:clock`** receives MIDI clock messages (0xFA, 0xFC, 0xFB) that affect channel0 only.
## ControlKey Modifier
Hold the **control key** (MIDI note64) pressed *before* sending another note to put the looper in “command mode”.
While controlkey is active, the next noteon (with velocity > 0) performs a special action instead of its direct mapping.
The control key is released either by sending noteoff (note64 or any note) or by sending a noteon while controlkey is already active (the action is performed and controlkey is cleared).
## Available Commands (under control key)
| Note | Action |
|------|----------------------------------------------------------------------------------------------|
| 015 | **Bind** the next `control+62` toggle to the channel with that index. |
| 60 | **Add** a new dynamic channel (creates `channelX_input` / `channelX_output` ports). |
| 61 | **Remove** the highestnumbered active channel (excluding channel0). |
| 62 | **Toggle** the current bound channel through its state machine: |
| | IDLE → RECORD → LOOPING → PAUSED → LOOPING → … (each press advances one step). |
| 63 | **Unbind** reset the bound channel back to **0**. |
> **Notes:**
> - The default bound channel is **0**. If you never send a bind command, `control+62` controls channel0.
> - To bind a different channel, send `control + note <16>` (e.g., control + note5 binds channel5).
> - Bind is sticky it stays until overwritten by another bind command.
> - To **unbind** (reset to channel0), send `control + note63`.
## Direct Mapping (without control key)
For backward compatibility, the following notes work **without** the controlkey modifier:
| Note | Action |
|------|----------------------------------------------------------------------------------------------|
| 1 | Toggle channel0 state (IDLE→RECORD→LOOPING→PAUSED→LOOPING…). |
| 60 | Add a dynamic channel (same as `control+60`). |
| 61 | Remove the highestnumbered active channel (same as `control+61`). |
## Example Usage
1. **Record a loop on channel0 (using direct note1)**
- Send noteon, note1, velocity127 → channel0 enters RECORD.
- Play some audio into `looper:input`.
- Send noteon, note1, velocity127 again → channel0 enters LOOPING.
- The recorded audio repeats indefinitely.
2. **Use the controlkey to toggle channel0**
- Send `noteon, note64` (control key).
- Then send `noteon, note62` → toggles channel0 (IDLE→RECORD).
- Send `noteon, note64` again, then `noteon, note62` again → RECORD→LOOPING.
3. **Add a new channel and bind it**
- Send `noteon, note64` + `noteon, note60` → creates channel1.
- Send `noteon, note64` + `noteon, note1` → binds channel1.
- Now `control+62` toggles channel1 instead of channel0.
- Record audio on channel1 by sending `control+62` twice.
4. **Remove a dynamic channel**
- Send `noteon, note64` + `noteon, note61` → removes the highestnumbered active channel (e.g., channel1).
## Notes
- The looper must be connected to a running JACK server.
- Channel buffers hold up to 5 seconds of audio at 48kHz.
- After removal, the channels audio ports are unregistered on the next mainloop cycle (deferred to avoid race conditions).
- The bind index is stored as an integer (015); values outside 015 are ignored (the note is processed as a command rather than a bind).

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

View File

Binary file not shown.

BIN
looper
View File

Binary file not shown.

View File

@@ -1,9 +1,15 @@
CC ?= gcc
CFLAGS ?= -Wall -Wextra -g
LDFLAGS ?= -ljack
CFLAGS ?= -Wall -Wextra -g -Isrc
LDFLAGS ?= -ljack -lm
looper: src/main.c
$(CC) $(CFLAGS) -o looper src/main.c $(LDFLAGS)
SRC = src/main.c src/looper.c src/channel.c src/midi.c
OBJ = $(SRC:.c=.o)
looper: $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
src/%.o: src/%.c
$(CC) $(CFLAGS) -c -o $@ $<
integration: looper tests/integration.c
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm
@@ -13,4 +19,14 @@ test: integration
.PHONY: clean integration test
clean:
rm -f looper integration_test
rm -f looper integration_test src/*.o
check:
cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix .
# Optional: Format code using clang-format
format:
clang-format -i src/*.c
install-hooks:
git config core.hooksPath .githooks

40
src/channel.c Normal file
View File

@@ -0,0 +1,40 @@
// cppcheck-suppress missingIncludeSystem
#include "channel.h"
#include <jack/jack.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.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 */
atomic_store(&channels[idx].active, 0);
return;
}
atomic_store(&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) {
(void)client;
atomic_store(&channels[idx].active, 0);
channel_count--;
}

40
src/channel.h Normal file
View File

@@ -0,0 +1,40 @@
#ifndef CHANNEL_H
#define CHANNEL_H
// cppcheck-suppress missingIncludeSystem
#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;
atomic_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

242
src/looper.c Normal file
View File

@@ -0,0 +1,242 @@
// cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include "channel.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) */
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;
atomic_int bind_channel = 0;
/* Deferred removal index (1 second grace) */
static int pending_unregister_idx = -1;
/* ----------------------------------------------------------------
* process callback
* ---------------------------------------------------------------- */
int process_callback(jack_nframes_t nframes, void *arg) {
(void)arg;
if (midi_control_port) {
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 (!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;
}
const jack_default_audio_sample_t *in =
(const 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) {
float *f_out = (float *)out;
const float *f_in = (const float *)in;
for (i = 0; i < nframes; i++) {
if (channels[c].record_pos < LOOP_BUF_SIZE)
channels[c].loop_buffer[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 (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 */
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
* ---------------------------------------------------------------- */
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) {
/* Unregister any ports that were marked for deferred removal.
By now the realtime thread has had at least one full cycle
to see the `active = 0` store. */
if (pending_unregister_idx != -1) {
int idx = pending_unregister_idx;
if (channels[idx].audio_in)
jack_port_unregister(client, channels[idx].audio_in);
if (channels[idx].audio_out)
jack_port_unregister(client, channels[idx].audio_out);
pending_unregister_idx = -1;
}
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) {
/* Mark inactive now; ports will be unregistered next round */
channel_remove(client, remove_idx);
pending_unregister_idx = remove_idx;
}
}
}

19
src/looper.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef LOOPER_H
#define LOOPER_H
// cppcheck-suppress missingIncludeSystem
#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,233 +1,51 @@
// cppcheck-suppress missingIncludeSystem
#include "looper.h"
#include <jack/jack.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdatomic.h>
#include <math.h>
#include <time.h>
#define LOOP_BUF_SIZE (5 * 48000) /* 5 seconds at 48 kHz, mono */
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
const char *client_name = "looper";
jack_options_t options = JackNullOption;
jack_status_t status;
typedef enum {
STATE_IDLE,
STATE_RECORD,
STATE_LOOPING,
STATE_PAUSED
} looper_state;
jack_client_t *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)
fprintf(stderr, "Unable to connect to JACK server\n");
return 1;
}
static atomic_int current_state = STATE_IDLE;
if (status & JackNameNotUnique)
client_name = jack_get_client_name(client);
/* 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);
}
int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
const char *client_name = "looper";
jack_options_t options = JackNullOption;
jack_status_t status;
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) {
fprintf(stderr, "Unable to connect to JACK server\n");
}
return 1;
}
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);
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;
}
if (jack_activate(client)) {
fprintf(stderr, "Cannot activate client\n");
return 1;
}
fprintf(stderr, "looper running (client name '%s')\n", client_name);
prev_state = -1; /* initialise change detection */
while (1) {
sleep(1);
}
jack_set_process_callback(client, process_callback, 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 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);
{ struct timespec ts = { .tv_sec = 0, .tv_nsec = 50000000 }; nanosleep(&ts, NULL); } /* check commands every 50 ms */
}
jack_client_close(client);
return 0;
}

111
src/midi.c Normal file
View File

@@ -0,0 +1,111 @@
// cppcheck-suppress missingIncludeSystem
#include "midi.h"
#include "channel.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdatomic.h>
extern atomic_int control_key_active;
extern atomic_int cmd_add;
extern atomic_int cmd_remove;
extern atomic_int bind_channel;
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);
if (note < 16) {
atomic_store(&bind_channel, note);
} else {
switch (note) {
case 60:
atomic_store(&cmd_add, 1);
break;
case 61:
atomic_store(&cmd_remove, 1);
break;
case 62: /* trigger looper channel via bind_channel */
{
int bch = atomic_load(&bind_channel);
if (bch >= 0 && bch < MAX_CHANNELS) {
int cur = atomic_load(&channels[bch].state);
switch (cur) {
case STATE_IDLE:
atomic_store(&channels[bch].state, STATE_RECORD);
break;
case STATE_RECORD:
atomic_store(&channels[bch].state, STATE_LOOPING);
break;
case STATE_LOOPING:
atomic_store(&channels[bch].state, STATE_PAUSED);
break;
case STATE_PAUSED:
atomic_store(&channels[bch].state, STATE_LOOPING);
break;
}
}
} break;
case 63: /* unbind reset bind to channel 0 */
atomic_store(&bind_channel, 0);
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);
}
}
}

9
src/midi.h Normal file
View File

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

View File

@@ -10,6 +10,7 @@
#include <jack/jack.h>
#include <jack/midiport.h>
#include <math.h>
#include <time.h>
/* static variables for passthrough test */
static jack_port_t *passthrough_output_port = NULL;
@@ -32,8 +33,16 @@ static jack_client_t *midi_inject_client = NULL;
static unsigned char midi_inject_note = 0;
static unsigned char midi_inject_velocity = 0;
static void safe_usleep(unsigned int usec) {
struct timespec ts;
ts.tv_sec = usec / 1000000;
ts.tv_nsec = (usec % 1000000) * 1000L;
nanosleep(&ts, NULL);
}
static int midi_inject_process(jack_nframes_t nframes, void *arg) {
(void)arg;
if (!midi_inject_port) return 0;
void *port_buf = jack_port_get_buffer(midi_inject_port, nframes);
if (!port_buf) return 0;
jack_midi_clear_buffer(port_buf);
@@ -56,11 +65,11 @@ static int passthrough_process(jack_nframes_t nframes, void *arg) {
(void)arg;
jack_default_audio_sample_t *out =
(jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_output_port, nframes);
jack_default_audio_sample_t *in =
(jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_input_port, nframes);
const jack_default_audio_sample_t *in =
(const jack_default_audio_sample_t *)jack_port_get_buffer(passthrough_input_port, nframes);
if (!out || !in) return 0;
float *outf = out;
const float *inf = in;
float *f_out = (float *)out;
const float *f_in = (const float *)in;
for (jack_nframes_t i = 0; i < nframes; i++) {
/* generate beep while beep_remaining > 0 or continuous sine */
float out_val;
@@ -73,17 +82,17 @@ static int passthrough_process(jack_nframes_t nframes, void *arg) {
} else {
out_val = 0.0f;
}
outf[i] = out_val;
f_out[i] = out_val;
/* detect bursts on the input (looper output) */
float sample = inf[i];
float sample = f_in[i];
int above = (fabsf(sample) > 0.05f);
if (above && !prev_above) {
bursts++;
}
prev_above = above;
passthrough_sum_sq += (double)inf[i] * (double)inf[i];
passthrough_sum_sq += (double)f_in[i] * (double)f_in[i];
passthrough_total_samples++;
}
if (passthrough_total_samples >= passthrough_sample_rate * 2) {
@@ -156,7 +165,7 @@ static int test_audio_pass_through(void) {
waitpid(pid, NULL, 0);
return 1;
}
usleep(200000);
safe_usleep(200000);
const char *looper_input = "looper:input";
const char *looper_output = "looper:output";
char my_output[64], my_input[64];
@@ -194,7 +203,7 @@ static int test_audio_pass_through(void) {
waitpid(pid, NULL, 0);
return 1;
}
usleep(2200000); /* 2.2 seconds */
safe_usleep(2200000);
int saw_input = passthrough_done;
double rms = passthrough_total_samples > 0 ?
sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0;
@@ -250,7 +259,7 @@ static int send_jack_note_on(const char *target_port, unsigned char note, unsign
}
/* wait for the process callback to clear the flag (event delivered) */
for (int attempts = 0; attempts < 50; attempts++) { /* ~50 * 10ms = 500ms */
usleep(10000);
safe_usleep(10000);
if (!midi_inject_pending) break;
}
jack_deactivate(midi_inject_client);
@@ -294,7 +303,7 @@ static int test_looper_looping(void) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
usleep(200000); /* wait for ports to appear */
safe_usleep(200000); /* wait for ports to appear */
/* connect test:out -> looper:input, looper:output -> test:in */
char my_out[64], my_in[64];
snprintf(my_out, sizeof(my_out), "test_looping:out");
@@ -312,7 +321,7 @@ static int test_looper_looping(void) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
usleep(500000); /* allow state to change (500ms) */
safe_usleep(500000); /* allow state to change (500ms) */
int sr = jack_get_sample_rate(client);
continuous_sine = 0; /* disable continuous tone */
@@ -336,10 +345,10 @@ static int test_looper_looping(void) {
return 1;
}
usleep(150000); /* let beep start */
safe_usleep(150000); /* let beep start */
/* ensure beep is fully captured */
usleep(800000); /* 0.8s after start of beep */
safe_usleep(800000); /* 0.8s after start of beep */
if (send_jack_note_on("looper:control", 1, 127) != 0) {
jack_client_close(client);
@@ -348,7 +357,7 @@ static int test_looper_looping(void) {
}
/* wait enough time for several loops (4 seconds to be safe) */
usleep(4000000);
safe_usleep(4000000);
jack_deactivate(client);
jack_client_close(client);
@@ -366,6 +375,466 @@ static int test_looper_looping(void) {
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) */
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;
}
/* test controlkey modifier (note 64 + note 62) */
static int test_control_key_modifier(void) {
printf("Test: controlkey modifier triggers state transition via note 62\n");
pid_t pid = start_looper();
if (pid < 0) return 1;
jack_client_t *client;
jack_status_t status;
client = jack_client_open("test_ctrl_key", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
/* connect same as in test_looper_looping but no beep generation */
jack_port_t *audio_out = jack_port_register(client, "out",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
jack_port_t *audio_in = jack_port_register(client, "in",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (!audio_out || !audio_in) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000);
char my_out[64], my_in[64];
snprintf(my_out, sizeof(my_out), "test_ctrl_key:out");
snprintf(my_in, sizeof(my_in), "test_ctrl_key:in");
if (jack_connect(client, my_out, "looper:input") ||
jack_connect(client, "looper:output", my_in)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
/* First send note 64 (control key) */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send note 64 failed\n");
return 1;
}
safe_usleep(200000);
/* Now send note 62 (toggle channel 0) */
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send note 62 failed\n");
return 1;
}
/* Wait for looper to enter RECORD and detect audio */
int sr = jack_get_sample_rate(client);
continuous_sine = 0;
beep_remaining = (int)(0.1f * sr); /* 0.1 second beep */
bursts = 0;
prev_above = 0;
passthrough_output_port = audio_out;
passthrough_input_port = audio_in;
passthrough_phase = 0.0f;
passthrough_freq = 440.0f;
passthrough_sample_rate = sr;
passthrough_total_samples = 0;
passthrough_sum_sq = 0.0;
passthrough_done = 0;
jack_set_process_callback(client, passthrough_process, NULL);
if (jack_activate(client)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000); /* allow beep */
/* send note 62 again under control key to move RECORD->LOOPING */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key resend\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send note 62 for loop\n");
return 1;
}
safe_usleep(2000000);
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
int got_bursts = bursts;
printf(" detected bursts: %d\n", got_bursts);
if (got_bursts < 3) {
fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts);
return 1;
}
printf(" PASS (controlkey modifier works)\n");
return 0;
}
/* test bind channel */
static int test_bind_channel(void) {
printf("Test: controlkey bind channel (note 0) and toggle\n");
pid_t pid = start_looper();
if (pid < 0) return 1;
jack_client_t *client;
jack_status_t status;
client = jack_client_open("test_bind", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
jack_port_t *audio_out = jack_port_register(client, "out",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
jack_port_t *audio_in = jack_port_register(client, "in",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (!audio_out || !audio_in) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000);
char my_out[64], my_in[64];
snprintf(my_out, sizeof(my_out), "test_bind:out");
snprintf(my_in, sizeof(my_in), "test_bind:in");
if (jack_connect(client, my_out, "looper:input") ||
jack_connect(client, "looper:output", my_in)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
/* Send control key + note 0 to bind to channel 0 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send control key failed\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 0, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send bind note 0 failed\n");
return 1;
}
safe_usleep(200000);
/* Now toggle using control+note62 should toggle channel 0 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send control key again failed\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send toggle note 62 failed\n");
return 1;
}
/* Wait and detect bursts as before */
int sr = jack_get_sample_rate(client);
continuous_sine = 0;
beep_remaining = (int)(0.1f * sr);
bursts = 0;
prev_above = 0;
passthrough_output_port = audio_out;
passthrough_input_port = audio_in;
passthrough_phase = 0.0f;
passthrough_freq = 440.0f;
passthrough_sample_rate = sr;
passthrough_total_samples = 0;
passthrough_sum_sq = 0.0;
passthrough_done = 0;
jack_set_process_callback(client, passthrough_process, NULL);
if (jack_activate(client)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000); /* allow beep */
/* send control+note62 again to move RECORD->LOOPING */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key for loop\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: toggle for loop\n");
return 1;
}
safe_usleep(2000000);
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
int got_bursts = bursts;
printf(" detected bursts: %d\n", got_bursts);
if (got_bursts < 3) {
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
return 1;
}
printf(" PASS (bind and toggle)\n");
return 0;
}
/* test unbind */
static int test_bind_unbind(void) {
printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n");
pid_t pid = start_looper();
if (pid < 0) return 1;
jack_client_t *client;
jack_status_t status;
client = jack_client_open("test_unbind", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
jack_port_t *audio_out = jack_port_register(client, "out",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
jack_port_t *audio_in = jack_port_register(client, "in",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (!audio_out || !audio_in) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000);
char my_out[64], my_in[64];
snprintf(my_out, sizeof(my_out), "test_unbind:out");
snprintf(my_in, sizeof(my_in), "test_unbind:in");
if (jack_connect(client, my_out, "looper:input") ||
jack_connect(client, "looper:output", my_in)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
/* Bind to channel 5 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send control key failed\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 5, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: bind to 5 failed\n");
return 1;
}
safe_usleep(200000);
/* Unbind (reset to 0) */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key for unbind\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 63, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send unbind note 63 failed\n");
return 1;
}
safe_usleep(200000);
/* Now toggle with control+62 should affect channel 0 */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key for toggle\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: toggle note 62\n");
return 1;
}
/* Wait for beep and loop */
int sr = jack_get_sample_rate(client);
continuous_sine = 0;
beep_remaining = (int)(0.1f * sr);
bursts = 0;
prev_above = 0;
passthrough_output_port = audio_out;
passthrough_input_port = audio_in;
passthrough_phase = 0.0f;
passthrough_freq = 440.0f;
passthrough_sample_rate = sr;
passthrough_total_samples = 0;
passthrough_sum_sq = 0.0;
passthrough_done = 0;
jack_set_process_callback(client, passthrough_process, NULL);
if (jack_activate(client)) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
return 1;
}
safe_usleep(200000); /* allow beep */
/* second control+62 -> loop */
if (send_jack_note_on("looper:control", 64, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: control key for loop\n");
return 1;
}
safe_usleep(200000);
if (send_jack_note_on("looper:control", 62, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: toggle for loop\n");
return 1;
}
safe_usleep(2000000);
jack_deactivate(client);
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
int got_bursts = bursts;
printf(" detected bursts: %d\n", got_bursts);
if (got_bursts < 3) {
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
return 1;
}
printf(" PASS (unbind works, toggle channel 0)\n");
return 0;
}
/* test remove channel */
static int test_remove_channel(void) {
printf("Test: dynamic channel removal 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_remove", JackNoStartServer, &status);
if (!client) {
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " SKIP: no JACK\n");
return 1;
}
/* add channel */
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;
}
safe_usleep(1500000);
/* verify channel1_input exists */
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
int found = 0;
if (ports) {
for (int i = 0; ports[i]; i++) {
if (strstr(ports[i], "looper:channel1_input")) {
found = 1;
break;
}
}
jack_free(ports);
}
if (!found) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: channel1_input not created\n");
return 1;
}
printf(" channel1_input created\n");
/* remove channel */
if (send_jack_note_on("looper:control", 61, 127) != 0) {
jack_client_close(client);
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
fprintf(stderr, " FAIL: send note 61 failed\n");
return 1;
}
safe_usleep(1500000);
/* verify channel1_input has disappeared */
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
int still_found = 0;
if (ports) {
for (int i = 0; ports[i]; i++) {
if (strstr(ports[i], "looper:channel1_input")) {
still_found = 1;
break;
}
}
jack_free(ports);
}
jack_client_close(client);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
if (still_found) {
fprintf(stderr, " FAIL: channel1_input not removed after remove command\n");
return 1;
}
printf(" PASS (channel removed)\n");
return 0;
}
int main(void) {
/* 1. binary must exist */
@@ -379,12 +848,48 @@ int main(void) {
/* 3. Audio passthrough test must work for basic connectivity */
test_audio_pass_through();
int failures = 0;
/* 4. Test that looping feature is now implemented */
if (test_looper_looping() != 0) {
fprintf(stderr, " FAILED\n");
return 1;
failures++;
}
/* 5. Test multiple dynamic channels */
if (test_multiple_channels() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
/* 6. Test controlkey modifier */
if (test_control_key_modifier() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
/* 7. Test bind channel */
if (test_bind_channel() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
/* 8. Test unbind */
if (test_bind_unbind() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
/* 9. Test channel removal */
if (test_remove_channel() != 0) {
fprintf(stderr, " FAILED\n");
failures++;
}
if (failures > 0) {
fprintf(stderr, "%d test(s) FAILED\n", failures);
return 1;
}
printf("All tests completed successfully.\n");
return 0;
}