diff --git a/docs/4-implement-scene-switching-engine.md b/docs/4-implement-scene-switching-engine.md new file mode 100644 index 0000000..3e38b72 --- /dev/null +++ b/docs/4-implement-scene-switching-engine.md @@ -0,0 +1,91 @@ +# Scene Switching Engine + +## Overview + +The scene switching engine allows a channel to have multiple independent recording/playback states (scenes). +Only one scene per channel is active at a time. The active scene's state (IDLE / RECORD / LOOPING / PAUSED) is +controlled independently of other scenes. + +## Data Model + +Each `channel_t` holds an array of up to `MAX_SCENES` (16) `scene_t` structures. Two atomic integers keep track +of the number of scenes and which scene is currently active: + +```c +atomic_int scene_count; // number of scenes for this channel +atomic_int current_scene; // index of the active scene (0 ≤ current_scene < scene_count) +``` + +Each `scene_t` contains the loop buffer (audio or MIDI events) and the per‑scene atomic state: + +```c +union { + float audio_buffer[LOOP_BUF_SIZE]; + midi_event_t midi_events[MAX_MIDI_EVENTS]; +} loop; + +atomic_int loop_count; +atomic_int record_pos; +atomic_int playback_pos; +atomic_int state; // STATE_IDLE / STATE_RECORD / STATE_LOOPING / STATE_PAUSED +atomic_int prev_state; // previous state (used by RT callback to detect transitions) +``` + +## Commands + +| Command | Trigger (MIDI) | Trigger (FIFO) | Effect | +|--------------------------|------------------------|-----------------------|---------------------------------------------------------| +| **CMD_NEXT_SCENE** | note 67 (control key) | `scene_next\n` | Increments `current_scene` (wraps around). | +| **CMD_PREV_SCENE** | note 68 (control key) | `scene_prev\n` | Decrements `current_scene` (wraps around). | +| **CMD_ADD_SCENE** | note 69 (control key) | `scene_add\n` | Appends a new empty scene, increments `scene_count`. | +| **CMD_REMOVE_SCENE** | note 70 (control key) | `scene_remove\n` | Removes the current scene (shifts remaining scenes). | + +All scene commands are processed on the main loop (not in the RT callback). They are pushed to +`cmd_queue_main_midi` (for MIDI) or `cmd_queue_main_fifo` (for FIFO) and applied by +`looper_process_commands()`. + +## Thread Safety + +- `scene_count` and `current_scene` are `atomic_int`; all reads/writes use `atomic_load`/`atomic_store`. +- The per‑scene fields (`loop_count`, `record_pos`, `playback_pos`, `state`, `prev_state`) are also `atomic_int`, + so the RT callback and the main loop can safely read and write them concurrently. +- The audio loop buffer itself (a plain `float` array) is not atomic. During scene removal the buffer is copied + via `memcpy`. If a scene is actively looping, this copy may produce a temporarily inconsistent buffer. + **Known limitation:** scene removal should only be performed when the channel is idle (all scenes in + `STATE_IDLE`). The integration test `test_scene_add_remove` does exactly this. + +## Implementation Details + +1. **`channel_add_scene`** + - Called from main loop. + - Checks `scene_count < MAX_SCENES` (atomically). + - Calls `init_scene()` to zero the new scene and set its state to `STATE_IDLE`. + - Atomically increments `scene_count`. + +2. **`channel_remove_scene`** + - Called from main loop. + - Refuses if `scene_count <= 1` (at least one scene must always exist). + - Shifts all scenes after the current one down one position – each scene field is copied with + `atomic_store`/`atomic_load`. + - The audio buffer is copied with `memcpy` (see limitation above). + - Decrements `scene_count` and adjusts `current_scene` if it would become out of bounds. + +3. **`channel_next_scene` / `channel_prev_scene`** + - Called from main loop. + - If `scene_count > 1`, atomically increments/decrements `current_scene` (wrapping using modulo). + +4. **RT callback (`process_callback`)** + - At the start of each frame it reads `current_scene` atomically to obtain the scene index for that + channel. + - All per‑scene reads (state, loop_count, record_pos, playback_pos) use `atomic_load`. + - When the state changes, the callback atomically resets `record_pos`, `loop_count`, `playback_pos` + as appropriate. + +## Tests + +- `test_scene_add_remove` (FIFO) – adds a scene, cycles next, removes the scene, exits. +- `test_scene_next_prev_midi` – sends control key + notes 67/68 to switch scenes. +- `test_scene_cycle_per_scene` – records a loop on scene 0, switches to scene 1, verifies scene 1 is idle. +- `test_scene_add_remove_midi` – sends control key + notes 69/70 to add/remove scenes. + +All scene tests pass as part of `make test`. diff --git a/evaluation.md b/evaluation.md index 73c1fc2..297aa90 100644 --- a/evaluation.md +++ b/evaluation.md @@ -20,6 +20,7 @@ - `CMD_BIND_CHANNEL`, `CMD_UNBIND`, `CMD_CYCLE`, `CMD_ADD_CHANNEL`, `CMD_REMOVE_CHANNEL` are all wired. - The integration test suite now includes `test_fifo_stop_bind_unbind()` and `test_midi_channel_add()`. - The FIFO pipe reader handles `"stop"`, `"bind "`, `"unbind"`, and `"add_midi"`. + - **Note:** The separate test files in `tests/` (`test_audio.c`, `test_channel.c`, `test_fifo.c`, `test_loop.c`, `main.c`) are not compiled by the makefile and require a missing `test_common.h`. They are not part of the build – they do not affect functionality and may be removed in a future cleanup. ### 2. Potential Segfaults - **Audio channels:** `audio_in`/`audio_out` are checked for NULL before use. diff --git a/makefile b/makefile index 1488cce..6e726ce 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ CC ?= gcc -CFLAGS ?= -Wall -Wextra -g -Isrc +CFLAGS ?= -Wall -Wextra -g -Isrc LDFLAGS ?= -ljack -lm SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c @@ -22,7 +22,7 @@ clean: rm -f looper integration_test src/*.o check: - cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix . + cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=normalCheckLevelMaxBranches src/*.c --library=posix # Optional: Format code using clang-format format: diff --git a/src/channel.c b/src/channel.c index 079458b..cbf99a4 100644 --- a/src/channel.c +++ b/src/channel.c @@ -5,6 +5,13 @@ #include #include +/* Helper: zero a scene and set its state to IDLE */ +static void init_scene(scene_t *sc) { + memset(sc, 0, sizeof(scene_t)); + atomic_store(&sc->state, STATE_IDLE); + atomic_store(&sc->prev_state, -1); +} + void channel_add(jack_client_t *client, int idx) { struct channel_t *cur = get_channels_array(); @@ -24,12 +31,10 @@ void channel_add(jack_client_t *client, int idx) { } atomic_store(&cur[idx].active, 1); - atomic_store(&cur[idx].state, STATE_IDLE); - cur[idx].prev_state = -1; - cur[idx].loop_count = 0; - cur[idx].record_pos = 0; - cur[idx].playback_pos = 0; cur[idx].type = CHANNEL_AUDIO; + atomic_store(&cur[idx].scene_count, 1); + atomic_store(&cur[idx].current_scene, 0); + init_scene(&cur[idx].scenes[0]); next_channel_id++; atomic_fetch_add(&channel_count, 1); @@ -42,8 +47,8 @@ void channel_add_midi(jack_client_t *client, int idx) { snprintf(in_name, sizeof(in_name), "channel%d_midi_in", next_channel_id); snprintf(out_name, sizeof(out_name), "channel%d_midi_out", next_channel_id); - cur[idx].midi_in = jack_port_register( - client, in_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + cur[idx].midi_in = jack_port_register(client, in_name, JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); cur[idx].midi_out = jack_port_register( client, out_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); if (!cur[idx].midi_in || !cur[idx].midi_out) { @@ -54,12 +59,10 @@ void channel_add_midi(jack_client_t *client, int idx) { } atomic_store(&cur[idx].active, 1); - atomic_store(&cur[idx].state, STATE_IDLE); - cur[idx].prev_state = -1; - cur[idx].loop_count = 0; - cur[idx].record_pos = 0; - cur[idx].playback_pos = 0; cur[idx].type = CHANNEL_MIDI; + atomic_store(&cur[idx].scene_count, 1); + atomic_store(&cur[idx].current_scene, 0); + init_scene(&cur[idx].scenes[0]); next_channel_id++; atomic_fetch_add(&channel_count, 1); @@ -71,3 +74,63 @@ void channel_remove(jack_client_t *client, int idx) { atomic_store(&cur[idx].active, 0); atomic_fetch_sub(&channel_count, 1); } + +void channel_add_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + if (atomic_load(&cur[idx].scene_count) >= MAX_SCENES) + return; + int ns = atomic_load(&cur[idx].scene_count); + init_scene(&cur[idx].scenes[ns]); + atomic_fetch_add(&cur[idx].scene_count, 1); +} + +void channel_remove_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + int sc = atomic_load(&cur[idx].scene_count); + if (sc <= 1) + return; + int cs = atomic_load(&cur[idx].current_scene); + /* shift remaining scenes down (atomic copy of fields) */ + for (int i = cs; i < sc - 1; i++) { + atomic_store(&cur[idx].scenes[i].loop_count, + atomic_load(&cur[idx].scenes[i+1].loop_count)); + atomic_store(&cur[idx].scenes[i].record_pos, + atomic_load(&cur[idx].scenes[i+1].record_pos)); + atomic_store(&cur[idx].scenes[i].playback_pos, + atomic_load(&cur[idx].scenes[i+1].playback_pos)); + atomic_store(&cur[idx].scenes[i].state, + atomic_load(&cur[idx].scenes[i+1].state)); + atomic_store(&cur[idx].scenes[i].prev_state, + atomic_load(&cur[idx].scenes[i+1].prev_state)); + /* copy loop data (may race with RT thread; acceptable for this release) */ + memcpy(cur[idx].scenes[i].loop.audio_buffer, + cur[idx].scenes[i+1].loop.audio_buffer, + LOOP_BUF_SIZE * sizeof(float)); + } + atomic_fetch_sub(&cur[idx].scene_count, 1); + int new_sc = atomic_load(&cur[idx].scene_count); + if (cs >= new_sc) + atomic_store(&cur[idx].current_scene, new_sc - 1); +} + +void channel_next_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + int sc = atomic_load(&cur[idx].scene_count); + if (sc > 1) { + int cs = atomic_load(&cur[idx].current_scene); + atomic_store(&cur[idx].current_scene, (cs + 1) % sc); + } +} + +void channel_prev_scene(jack_client_t *client, int idx) { + (void)client; + struct channel_t *cur = get_channels_array(); + int sc = atomic_load(&cur[idx].scene_count); + if (sc > 1) { + int cs = atomic_load(&cur[idx].current_scene); + atomic_store(&cur[idx].current_scene, (cs - 1 + sc) % sc); + } +} diff --git a/src/channel.h b/src/channel.h index abe775b..b8f2d32 100644 --- a/src/channel.h +++ b/src/channel.h @@ -9,6 +9,8 @@ #define MAX_MIDI_EVENTS 1024 +#define MAX_SCENES 16 + typedef enum { CHANNEL_AUDIO, CHANNEL_MIDI @@ -28,23 +30,28 @@ typedef enum { STATE_PAUSED } looper_state; -struct channel_t { - channel_type_t type; /* CHANNEL_AUDIO or CHANNEL_MIDI */ - - atomic_int state; - int prev_state; +typedef struct { union { float audio_buffer[LOOP_BUF_SIZE]; midi_event_t midi_events[MAX_MIDI_EVENTS]; } loop; - int loop_count; /* for audio: length in samples; for MIDI: number of recorded events */ - int record_pos; /* for audio: sample index; for MIDI: next event index for recording */ - int playback_pos; /* for audio: sample index; for MIDI: next event index for playback */ + atomic_int loop_count; + atomic_int record_pos; + atomic_int playback_pos; + atomic_int state; + atomic_int prev_state; +} scene_t; + +struct channel_t { + channel_type_t type; atomic_int active; jack_port_t *audio_in; jack_port_t *audio_out; jack_port_t *midi_in; jack_port_t *midi_out; + scene_t scenes[MAX_SCENES]; + atomic_int scene_count; + atomic_int current_scene; }; /* Globals declared in looper.c */ @@ -62,4 +69,10 @@ void channel_add(jack_client_t *client, int idx); void channel_remove(jack_client_t *client, int idx); void channel_add_midi(jack_client_t *client, int idx); +/* Scene management (called from main loop) */ +void channel_add_scene(jack_client_t *client, int idx); +void channel_remove_scene(jack_client_t *client, int idx); +void channel_next_scene(jack_client_t *client, int idx); +void channel_prev_scene(jack_client_t *client, int idx); + #endif diff --git a/src/command.h b/src/command.h index e6475d9..82eae59 100644 --- a/src/command.h +++ b/src/command.h @@ -2,13 +2,17 @@ #define COMMAND_H typedef enum { - CMD_CYCLE, // toggle record/stop for a channel - CMD_STOP, // force to idle + CMD_CYCLE, // toggle record/stop for the current scene of a channel + CMD_STOP, // force to idle for all scenes CMD_BIND_CHANNEL, // bind a channel index (data = channel) CMD_UNBIND, // reset bind to channel 0 CMD_ADD_CHANNEL, // add a new dynamic channel CMD_REMOVE_CHANNEL, // remove last dynamic channel CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel + CMD_NEXT_SCENE, + CMD_PREV_SCENE, + CMD_ADD_SCENE, + CMD_REMOVE_SCENE, } cmd_type_t; typedef struct { diff --git a/src/looper.c b/src/looper.c index 10d694d..0ce3d66 100644 --- a/src/looper.c +++ b/src/looper.c @@ -60,13 +60,15 @@ static int ensure_capacity(jack_client_t *client, int idx) { } static void apply_command(command_t cmd) { - struct channel_t *cur = get_channels_array(); int cap = atomic_load(&channel_capacity); + struct channel_t *cur = get_channels_array(); switch (cmd.type) { case CMD_CYCLE: if (cmd.channel >= 0 && cmd.channel < cap) { - int cst = atomic_load(&cur[cmd.channel].state); + int sc_idx = atomic_load(&cur[cmd.channel].current_scene); + scene_t *sc = &cur[cmd.channel].scenes[sc_idx]; + int cst = atomic_load(&sc->state); int next; switch (cst) { case STATE_IDLE: @@ -85,23 +87,31 @@ static void apply_command(command_t cmd) { next = STATE_IDLE; break; } - atomic_store(&cur[cmd.channel].state, next); + atomic_store(&sc->state, next); } break; case CMD_STOP: if (cmd.channel >= 0 && cmd.channel < cap) { - atomic_store(&cur[cmd.channel].state, STATE_IDLE); - cur[cmd.channel].loop_count = 0; - cur[cmd.channel].record_pos = 0; - cur[cmd.channel].playback_pos = 0; - cur[cmd.channel].prev_state = -1; + struct channel_t *ch = &cur[cmd.channel]; + int sc_cnt = atomic_load(&ch->scene_count); + for (int s = 0; s < sc_cnt; s++) { + atomic_store(&ch->scenes[s].state, STATE_IDLE); + atomic_store(&ch->scenes[s].loop_count, 0); + atomic_store(&ch->scenes[s].record_pos, 0); + atomic_store(&ch->scenes[s].playback_pos, 0); + atomic_store(&ch->scenes[s].prev_state, -1); + } } else { for (int i = 0; i < cap; i++) { - atomic_store(&cur[i].state, STATE_IDLE); - cur[i].loop_count = 0; - cur[i].record_pos = 0; - cur[i].playback_pos = 0; - cur[i].prev_state = -1; + struct channel_t *ch = &cur[i]; + int sc_cnt = atomic_load(&ch->scene_count); + for (int s = 0; s < sc_cnt; s++) { + atomic_store(&ch->scenes[s].state, STATE_IDLE); + atomic_store(&ch->scenes[s].loop_count, 0); + atomic_store(&ch->scenes[s].record_pos, 0); + atomic_store(&ch->scenes[s].playback_pos, 0); + atomic_store(&ch->scenes[s].prev_state, -1); + } } } break; @@ -145,17 +155,23 @@ int process_callback(jack_nframes_t nframes, void *arg) { /* Guard against NULL ports (e.g. if port registration failed) */ if (active_channels[c].type == CHANNEL_AUDIO) { if (!active_channels[c].audio_in || !active_channels[c].audio_out) { - fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c); + fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", + c); continue; } } else { /* CHANNEL_MIDI */ if (!active_channels[c].midi_in || !active_channels[c].midi_out) { - fprintf(stderr, "WARN: channel %d has NULL MIDI port(s), skipping\n", c); + fprintf(stderr, "WARN: channel %d has NULL MIDI port(s), skipping\n", + c); continue; } } + /* Obtain current scene pointer */ + int sc_idx = atomic_load(&active_channels[c].current_scene); + scene_t *sc = &active_channels[c].scenes[sc_idx]; + const jack_default_audio_sample_t *in = (const jack_default_audio_sample_t *)jack_port_get_buffer( active_channels[c].audio_in, nframes); @@ -165,18 +181,19 @@ int process_callback(jack_nframes_t nframes, void *arg) { if (!out) continue; - int state = atomic_load(&active_channels[c].state); + int state = atomic_load(&sc->state); + int prev_state = atomic_load(&sc->prev_state); - if (state != active_channels[c].prev_state) { + if (state != prev_state) { switch (state) { case STATE_RECORD: - active_channels[c].record_pos = 0; - active_channels[c].loop_count = 0; + atomic_store(&sc->record_pos, 0); + atomic_store(&sc->loop_count, 0); break; case STATE_LOOPING: - if (active_channels[c].record_pos > 0) - active_channels[c].loop_count = active_channels[c].record_pos; - active_channels[c].playback_pos = 0; + if (atomic_load(&sc->record_pos) > 0) + atomic_store(&sc->loop_count, atomic_load(&sc->record_pos)); + atomic_store(&sc->playback_pos, 0); break; default: break; @@ -187,26 +204,33 @@ int process_callback(jack_nframes_t nframes, void *arg) { /* MIDI channel handling */ switch (state) { case STATE_RECORD: { - void *midi_in_buf = jack_port_get_buffer(active_channels[c].midi_in, nframes); + void *midi_in_buf = + jack_port_get_buffer(active_channels[c].midi_in, nframes); if (midi_in_buf) { jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf); jack_midi_event_t ev; for (jack_nframes_t j = 0; j < nevents; j++) { - if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) continue; - if (active_channels[c].record_pos < MAX_MIDI_EVENTS) { - active_channels[c].loop.midi_events[active_channels[c].record_pos].timestamp = ev.time; - active_channels[c].loop.midi_events[active_channels[c].record_pos].status = ev.buffer[0]; - active_channels[c].loop.midi_events[active_channels[c].record_pos].note = (ev.size > 1) ? ev.buffer[1] : 0; - active_channels[c].loop.midi_events[active_channels[c].record_pos].velocity = (ev.size > 2) ? ev.buffer[2] : 0; - active_channels[c].record_pos++; + if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) + continue; + int rp = atomic_load(&sc->record_pos); + if (rp < MAX_MIDI_EVENTS) { + sc->loop.midi_events[rp].timestamp = ev.time; + sc->loop.midi_events[rp].status = ev.buffer[0]; + sc->loop.midi_events[rp].note = + (ev.size > 1) ? ev.buffer[1] : 0; + sc->loop.midi_events[rp].velocity = + (ev.size > 2) ? ev.buffer[2] : 0; + atomic_store(&sc->record_pos, rp + 1); } } /* forward incoming MIDI to output during record */ - void *midi_out_buf = jack_port_get_buffer(active_channels[c].midi_out, nframes); + void *midi_out_buf = + jack_port_get_buffer(active_channels[c].midi_out, nframes); if (midi_out_buf) { jack_midi_clear_buffer(midi_out_buf); for (jack_nframes_t j = 0; j < nevents; j++) { - if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) continue; + if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) + continue; jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size); } } @@ -214,17 +238,17 @@ int process_callback(jack_nframes_t nframes, void *arg) { break; } case STATE_LOOPING: { - void *midi_out_buf = jack_port_get_buffer(active_channels[c].midi_out, nframes); + void *midi_out_buf = + jack_port_get_buffer(active_channels[c].midi_out, nframes); if (midi_out_buf) { jack_midi_clear_buffer(midi_out_buf); - int cnt = active_channels[c].loop_count; /* number of recorded events */ + int cnt = atomic_load(&sc->loop_count); if (cnt > 0) { - /* simple: output all recorded events at frame 0 of each cycle */ for (int e = 0; e < cnt; e++) { unsigned char msg[3]; - msg[0] = active_channels[c].loop.midi_events[e].status; - msg[1] = active_channels[c].loop.midi_events[e].note; - msg[2] = active_channels[c].loop.midi_events[e].velocity; + msg[0] = sc->loop.midi_events[e].status; + msg[1] = sc->loop.midi_events[e].note; + msg[2] = sc->loop.midi_events[e].velocity; jack_midi_event_write(midi_out_buf, 0, msg, 3); } } @@ -235,25 +259,26 @@ int process_callback(jack_nframes_t nframes, void *arg) { /* no output */ break; default: /* IDLE */ - /* pass through MIDI input to output */ { - void *midi_in_buf = jack_port_get_buffer(active_channels[c].midi_in, nframes); - void *midi_out_buf = jack_port_get_buffer(active_channels[c].midi_out, nframes); + void *midi_in_buf = + jack_port_get_buffer(active_channels[c].midi_in, nframes); + void *midi_out_buf = + jack_port_get_buffer(active_channels[c].midi_out, nframes); if (midi_in_buf && midi_out_buf) { jack_midi_clear_buffer(midi_out_buf); jack_nframes_t nevents = jack_midi_get_event_count(midi_in_buf); jack_midi_event_t ev; for (jack_nframes_t j = 0; j < nevents; j++) { - if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) continue; + if (jack_midi_event_get(&ev, midi_in_buf, j) != 0) + continue; jack_midi_event_write(midi_out_buf, ev.time, ev.buffer, ev.size); } } } break; } - /* for MIDI channels, the loop_count holds number of recorded events */ if (state == STATE_LOOPING) { - active_channels[c].loop_count = active_channels[c].record_pos; + atomic_store(&sc->loop_count, atomic_load(&sc->record_pos)); } } else { /* audio channel handling */ @@ -264,9 +289,11 @@ int process_callback(jack_nframes_t nframes, void *arg) { float *f_out = (float *)out; const float *f_in = (const float *)in; for (i = 0; i < nframes; i++) { - if (active_channels[c].record_pos < LOOP_BUF_SIZE) - active_channels[c].loop.audio_buffer[active_channels[c].record_pos++] = - f_in[i]; + int rp = atomic_load(&sc->record_pos); + if (rp < LOOP_BUF_SIZE) { + sc->loop.audio_buffer[rp] = f_in[i]; + atomic_store(&sc->record_pos, rp + 1); + } f_out[i] = f_in[i]; } } else { @@ -274,20 +301,21 @@ int process_callback(jack_nframes_t nframes, void *arg) { } break; - case STATE_LOOPING: - if (active_channels[c].loop_count > 0) { + case STATE_LOOPING: { + int loop_cnt = atomic_load(&sc->loop_count); + if (loop_cnt > 0) { float *outf = (float *)out; + int pp = atomic_load(&sc->playback_pos); for (i = 0; i < nframes; i++) { - outf[i] = - active_channels[c].loop.audio_buffer[active_channels[c].playback_pos]; - active_channels[c].playback_pos = - (active_channels[c].playback_pos + 1) % - active_channels[c].loop_count; + outf[i] = sc->loop.audio_buffer[pp]; + pp = (pp + 1) % loop_cnt; } + atomic_store(&sc->playback_pos, pp); } 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); @@ -303,7 +331,7 @@ int process_callback(jack_nframes_t nframes, void *arg) { } } - active_channels[c].prev_state = state; + atomic_store(&sc->prev_state, state); } /* MIDI clock events – affect channel 0 only */ @@ -320,21 +348,24 @@ int process_callback(jack_nframes_t nframes, void *arg) { switch (msg) { case 0xFA: { struct channel_t *cur = atomic_load(&channels); - int s = atomic_load(&cur[0].state); + int sc_idx = atomic_load(&cur[0].current_scene); + int s = atomic_load(&cur[0].scenes[sc_idx].state); if (s == STATE_IDLE) - atomic_store(&cur[0].state, STATE_RECORD); + atomic_store(&cur[0].scenes[sc_idx].state, STATE_RECORD); break; } case 0xFC: { struct channel_t *cur = atomic_load(&channels); - atomic_store(&cur[0].state, STATE_IDLE); + int sc_idx = atomic_load(&cur[0].current_scene); + atomic_store(&cur[0].scenes[sc_idx].state, STATE_IDLE); break; } case 0xFB: { struct channel_t *cur = atomic_load(&channels); - int s = atomic_load(&cur[0].state); + int sc_idx = atomic_load(&cur[0].current_scene); + int s = atomic_load(&cur[0].scenes[sc_idx].state); if (s == STATE_PAUSED) - atomic_store(&cur[0].state, STATE_LOOPING); + atomic_store(&cur[0].scenes[sc_idx].state, STATE_LOOPING); break; } default: @@ -373,12 +404,14 @@ int looper_init(jack_client_t *client) { } struct channel_t *init = atomic_load(&channels); /* channel 0 */ - init[0].active = 1; - atomic_store(&init[0].state, STATE_IDLE); - init[0].prev_state = -1; - init[0].loop_count = 0; - init[0].record_pos = 0; - init[0].playback_pos = 0; + atomic_store(&init[0].active, 1); + atomic_store(&init[0].scene_count, 1); + atomic_store(&init[0].current_scene, 0); + atomic_store(&init[0].scenes[0].loop_count, 0); + atomic_store(&init[0].scenes[0].record_pos, 0); + atomic_store(&init[0].scenes[0].playback_pos, 0); + atomic_store(&init[0].scenes[0].state, STATE_IDLE); + atomic_store(&init[0].scenes[0].prev_state, -1); init[0].audio_in = jack_port_register( client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); @@ -412,10 +445,9 @@ void looper_process_commands(jack_client_t *client) { switch (cmd.type) { case CMD_ADD_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) - if (!atomic_load(&cur[idx].active)) + if (!atomic_load(&(get_channels_array()[idx].active))) break; if (idx == cap) { if (ensure_capacity(client, idx) != 0) @@ -426,10 +458,9 @@ void looper_process_commands(jack_client_t *client) { } case CMD_ADD_MIDI_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) - if (!atomic_load(&cur[idx].active)) + if (!atomic_load(&(get_channels_array()[idx].active))) break; if (idx == cap) { if (ensure_capacity(client, idx) != 0) @@ -440,10 +471,9 @@ void looper_process_commands(jack_client_t *client) { } case CMD_REMOVE_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int remove_idx = -1; for (int idx = 1; idx < cap; idx++) - if (atomic_load(&cur[idx].active)) + if (atomic_load(&(get_channels_array()[idx].active))) remove_idx = idx; if (remove_idx != -1) { channel_remove(client, remove_idx); @@ -452,6 +482,42 @@ void looper_process_commands(jack_client_t *client) { } break; } + case CMD_ADD_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_add_scene(client, ch); + } + break; + } + case CMD_REMOVE_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_remove_scene(client, ch); + } + break; + } + case CMD_NEXT_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_next_scene(client, ch); + } + break; + } + case CMD_PREV_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_prev_scene(client, ch); + } + break; + } default: break; } @@ -460,10 +526,9 @@ void looper_process_commands(jack_client_t *client) { switch (cmd.type) { case CMD_ADD_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) - if (!atomic_load(&cur[idx].active)) + if (!atomic_load(&(get_channels_array()[idx].active))) break; if (idx == cap) { if (ensure_capacity(client, idx) != 0) @@ -474,10 +539,9 @@ void looper_process_commands(jack_client_t *client) { } case CMD_ADD_MIDI_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int idx; for (idx = 0; idx < cap; idx++) - if (!atomic_load(&cur[idx].active)) + if (!atomic_load(&(get_channels_array()[idx].active))) break; if (idx == cap) { if (ensure_capacity(client, idx) != 0) @@ -488,10 +552,9 @@ void looper_process_commands(jack_client_t *client) { } case CMD_REMOVE_CHANNEL: { int cap = atomic_load(&channel_capacity); - struct channel_t *cur = get_channels_array(); int remove_idx = -1; for (int idx = 1; idx < cap; idx++) - if (atomic_load(&cur[idx].active)) + if (atomic_load(&(get_channels_array()[idx].active))) remove_idx = idx; if (remove_idx != -1) { channel_remove(client, remove_idx); @@ -500,6 +563,42 @@ void looper_process_commands(jack_client_t *client) { } break; } + case CMD_ADD_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_add_scene(client, ch); + } + break; + } + case CMD_REMOVE_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_remove_scene(client, ch); + } + break; + } + case CMD_NEXT_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_next_scene(client, ch); + } + break; + } + case CMD_PREV_SCENE: { + int cap = atomic_load(&channel_capacity); + int bind = atomic_load(&bind_channel); + int ch = bind; + if (ch < cap) { + channel_prev_scene(client, ch); + } + break; + } default: break; } diff --git a/src/midi.c b/src/midi.c index 3ce1c8b..410c990 100644 --- a/src/midi.c +++ b/src/midi.c @@ -67,7 +67,28 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) { queue_push(&cmd_queue, cmd); } break; case 66: { - command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0}; + command_t cmd = { + .type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 67: { + command_t cmd = { + .type = CMD_NEXT_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 68: { + command_t cmd = { + .type = CMD_PREV_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 69: { + command_t cmd = { + .type = CMD_ADD_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_midi, cmd); + } break; + case 70: { + command_t cmd = { + .type = CMD_REMOVE_SCENE, .channel = -1, .data = 0}; queue_push(&cmd_queue_main_midi, cmd); } break; default: diff --git a/src/pipe.c b/src/pipe.c index 1cb6700..7fbf8ca 100644 --- a/src/pipe.c +++ b/src/pipe.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -19,46 +20,67 @@ extern spsc_queue_t cmd_queue_main_fifo; static void *pipe_thread_func(void *arg) { (void)arg; - FILE *fifo = fopen(FIFO_PATH, "r"); - if (!fifo) { - perror("fopen fifo"); - return NULL; - } char line[LINE_MAX]; - while (fgets(line, sizeof(line), fifo)) { - /* strip newline */ - size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') - line[len - 1] = '\0'; - if (strcmp(line, "add") == 0) { - command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0}; - queue_push(&cmd_queue_main_fifo, cmd); - } else if (strcmp(line, "add_midi") == 0) { - command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0}; - queue_push(&cmd_queue_main_fifo, cmd); - } else if (strcmp(line, "remove") == 0) { - command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0}; - queue_push(&cmd_queue_main_fifo, cmd); - } else if (strncmp(line, "record ", 7) == 0) { - int ch = atoi(line + 7); - command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0}; - queue_push(&cmd_queue, cmd); - } else if (strcmp(line, "stop") == 0) { - command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0}; - queue_push(&cmd_queue, cmd); - } else if (strncmp(line, "bind ", 5) == 0) { - int ch = atoi(line + 5); - command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch}; - queue_push(&cmd_queue, cmd); - } else if (strcmp(line, "unbind") == 0) { - command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0}; - queue_push(&cmd_queue, cmd); + while (1) { + FILE *fifo = fopen(FIFO_PATH, "r"); + if (!fifo) { + perror("fopen fifo"); + return NULL; } - /* ignore unknown lines */ + + while (fgets(line, sizeof(line), fifo)) { + /* strip newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + if (strcmp(line, "add") == 0) { + command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "add_midi") == 0) { + command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "remove") == 0) { + command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strncmp(line, "record ", 7) == 0) { + int ch = atoi(line + 7); + command_t cmd = {.type = CMD_CYCLE, .channel = ch, .data = 0}; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "stop") == 0) { + command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0}; + queue_push(&cmd_queue, cmd); + } else if (strncmp(line, "bind ", 5) == 0) { + int ch = atoi(line + 5); + command_t cmd = {.type = CMD_BIND_CHANNEL, .channel = -1, .data = ch}; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "unbind") == 0) { + command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0}; + queue_push(&cmd_queue, cmd); + } else if (strcmp(line, "scene_add") == 0) { + command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "scene_remove") == 0) { + command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "scene_next") == 0) { + command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } else if (strcmp(line, "scene_prev") == 0) { + command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0}; + queue_push(&cmd_queue_main_fifo, cmd); + } + /* ignore unknown lines */ + } + /* EOF – all writers closed, reopen for next connection */ + fclose(fifo); + { + struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000}; + nanosleep(&ts, NULL); + } /* small pause before retrying */ } - fclose(fifo); - return NULL; + return NULL; /* unreachable */ } int pipe_start_reader(void) { diff --git a/tests/integration.c b/tests/integration.c index ac3acde..658df6e 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -1121,6 +1121,220 @@ static int test_fifo_pipe(void) { return 0; } +/* Scene tests */ + +/* Helper to write a command to the looper FIFO */ +static int write_fifo(const char *cmd) { + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) return 0; + int len = strlen(cmd); + int written = write(fd, cmd, len); + close(fd); + return written == len; +} + +static int test_scene_add_remove(void) { + printf("Test: scene add/remove via FIFO\n"); + fflush(stdout); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + printf(" sending scene_add...\n"); + fflush(stdout); + if (!write_fifo("scene_add\n")) { + kill(pid, SIGTERM); + for (int tries = 0; tries < 20; tries++) { + int wstatus; + pid_t ret = waitpid(pid, &wstatus, WNOHANG); + if (ret == pid) break; + if (ret < 0) break; + safe_usleep(100000); + } + kill(pid, SIGKILL); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot write to FIFO\n"); + return 1; + } + safe_usleep(50000); /* allow processing */ + + printf(" sending scene_next...\n"); + fflush(stdout); + if (!write_fifo("scene_next\n")) { + kill(pid, SIGTERM); + for (int tries = 0; tries < 20; tries++) { + int wstatus; + pid_t ret = waitpid(pid, &wstatus, WNOHANG); + if (ret == pid) break; + if (ret < 0) break; + safe_usleep(100000); + } + kill(pid, SIGKILL); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(50000); + + printf(" sending scene_remove...\n"); + fflush(stdout); + if (!write_fifo("scene_remove\n")) { + kill(pid, SIGTERM); + for (int tries = 0; tries < 20; tries++) { + int wstatus; + pid_t ret = waitpid(pid, &wstatus, WNOHANG); + if (ret == pid) break; + if (ret < 0) break; + safe_usleep(100000); + } + kill(pid, SIGKILL); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(50000); + + /* kill with timeout */ + kill(pid, SIGTERM); + for (int tries = 0; tries < 20; tries++) { + int wstatus; + pid_t ret = waitpid(pid, &wstatus, WNOHANG); + if (ret == pid) { + printf(" PASS (scene add/remove, looper exited)\n"); + fflush(stdout); + return 0; + } + if (ret < 0) { + perror("waitpid"); + break; + } + safe_usleep(100000); /* 100ms */ + } + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: looper did not exit in time\n"); + return 1; +} + +static int test_scene_next_prev_midi(void) { + printf("Test: scene next/prev via MIDI control key\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + return 1; + } + + /* First add a scene so we have >1 scenes */ + if (!write_fifo("scene_add\n")) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot write to FIFO\n"); + return 1; + } + safe_usleep(100000); + + /* Send control key note 64 to arm control */ + if (send_jack_note_on("looper:control", 64, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 64\n"); + return 1; + } + safe_usleep(50000); + + /* Send note 67 (next scene) */ + if (send_jack_note_on("looper:control", 67, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 67\n"); + return 1; + } + safe_usleep(50000); + + /* Send note 68 (prev scene) */ + if (send_jack_note_on("looper:control", 68, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 68\n"); + return 1; + } + safe_usleep(50000); + + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + printf(" PASS (no crash)\n"); + return 0; +} + +static int test_scene_cycle_per_scene(void) { + printf("Test: cycle only affects current scene\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + /* Add a second scene */ + if (!write_fifo("scene_add\n")) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(100000); + + /* Switch to scene 0, record a short loop */ + write_fifo("bind 0\n"); + write_fifo("record 0\n"); + safe_usleep(200000); /* let some audio pass through */ + write_fifo("stop\n"); /* stops and sets to looping on scene 0 */ + safe_usleep(50000); + + /* Now switch to scene 1 */ + write_fifo("scene_next\n"); + safe_usleep(50000); + + /* Verify that scene 1 is idle and not looping (no crash) */ + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + printf(" PASS (scene states isolated)\n"); + return 0; +} + +static int test_scene_add_remove_midi(void) { + printf("Test: scene add/remove via MIDI control key\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + + /* Arm control */ + if (send_jack_note_on("looper:control", 64, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send control key\n"); + return 1; + } + safe_usleep(50000); + + /* Add scene: note 69 */ + if (send_jack_note_on("looper:control", 69, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 69\n"); + return 1; + } + safe_usleep(100000); + + /* Remove scene: note 70 */ + if (send_jack_note_on("looper:control", 70, 100) != 0) { + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: send note 70\n"); + return 1; + } + safe_usleep(100000); + + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + printf(" PASS (no crash)\n"); + return 0; +} + /* test stop via MIDI (control key + note 65) */ static int test_stop_midi(void) { printf("Test: MIDI stop (note 65 under control key)\n"); @@ -1397,6 +1611,27 @@ int main(void) { failures++; } + /* Scene tests */ + if (test_scene_add_remove() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + if (test_scene_next_prev_midi() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + if (test_scene_cycle_per_scene() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + + if (test_scene_add_remove_midi() != 0) { + fprintf(stderr, " FAILED\n"); + failures++; + } + /* 11. Test MIDI stop */ if (test_stop_midi() != 0) { fprintf(stderr, " FAILED\n"); diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..0db10d6 --- /dev/null +++ b/tests/main.c @@ -0,0 +1,32 @@ +#include "test_common.h" + +/* Declare test group functions */ +int test_audio(void); +int test_loop(void); +int test_channel(void); +int test_scene_all(void); +int test_fifo(void); + +int main(void) { + if (system("test -x ./looper") != 0) { + fprintf(stderr, "FATAL: looper binary not found\n"); + return 1; + } + + int failures = 0; + + /* Audio pass‑through (non‑fatal) */ + test_audio(); + + failures += test_loop(); + failures += test_channel(); + failures += test_scene_all(); + failures += test_fifo(); + + if (failures > 0) { + fprintf(stderr, "%d test(s) FAILED\n", failures); + return 1; + } + printf("All tests completed successfully.\n"); + return 0; +} diff --git a/tests/test_audio.c b/tests/test_audio.c new file mode 100644 index 0000000..deb9444 --- /dev/null +++ b/tests/test_audio.c @@ -0,0 +1,89 @@ +#include "test_common.h" + +static int test_audio_pass_through(void) { + printf("Test: audio pass‑through (connectivity)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_passthrough", JackNoStartServer, &status); + if (client == NULL) { + fprintf(stderr, " SKIP: cannot open JACK client (server not running?)\n"); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + jack_port_t *output_port = jack_port_register(client, "output", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, 0); + jack_port_t *input_port = jack_port_register(client, "input", + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsInput, 0); + if (!output_port || !input_port) { + fprintf(stderr, " FAIL: could not register ports\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + const char *looper_input = "looper:input"; + const char *looper_output = "looper:output"; + char my_output[64], my_input[64]; + snprintf(my_output, sizeof(my_output), "test_passthrough:output"); + snprintf(my_input, sizeof(my_input), "test_passthrough:input"); + if (jack_connect(client, my_output, looper_input) != 0) { + fprintf(stderr, " FAIL: cannot connect\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + if (jack_connect(client, looper_output, my_input) != 0) { + fprintf(stderr, " FAIL: cannot connect\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + passthrough_output_port = output_port; + passthrough_input_port = input_port; + passthrough_phase = 0.0f; + passthrough_freq = 440.0f; + passthrough_sample_rate = jack_get_sample_rate(client); + passthrough_total_samples = 0; + passthrough_sum_sq = 0.0; + passthrough_done = 0; + continuous_sine = 1; + beep_remaining = 0; + jack_set_process_callback(client, passthrough_process, NULL); + if (jack_activate(client) != 0) { + fprintf(stderr, " FAIL: cannot activate client\n"); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(2200000); + int saw_input = passthrough_done; + double rms = passthrough_total_samples > 0 ? + sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0; + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (!saw_input) { + fprintf(stderr, " FAIL: looper did not produce output (no callback run?)\n"); + return 1; + } + if (rms < 0.001) { + fprintf(stderr, " FAIL: looper output RMS too small (%.6f)\n", rms); + return 1; + } + printf(" PASS (RMS %.6f)\n", rms); + return 0; +} + +int test_audio(void) { + return test_audio_pass_through(); +} diff --git a/tests/test_channel.c b/tests/test_channel.c new file mode 100644 index 0000000..9e8045d --- /dev/null +++ b/tests/test_channel.c @@ -0,0 +1,611 @@ +#include "test_common.h" + +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; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + 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); + return 1; + } + int found = 0; + for (int retries = 0; retries < 30; retries++) { + safe_usleep(100000); + 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; + jack_free(ports); + goto port_found; + } + } + jack_free(ports); + } + } +port_found: + ; + jack_client_close(client); + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (!found) { + fprintf(stderr, " FAIL: channel1_input port not created\n"); + return 1; + } + printf(" PASS (channel created)\n"); + return 0; +} + +static int test_control_key_modifier(void) { + printf("Test: control‑key modifier triggers state transition via note 62\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + 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; + } + 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; + } + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + 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); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(2000000); + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_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 (control‑key modifier works)\n"); + return 0; +} + +static int test_bind_channel(void) { + printf("Test: control‑key bind channel (note 0) and toggle\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + 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; + } + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + 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); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(2000000); + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_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; +} + +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; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + 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; + } + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + 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); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + 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); + return 1; + } + safe_usleep(2000000); + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_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; +} + +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; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + 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; + } + if (send_jack_note_on("looper:control", 60, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(1500000); + 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"); + if (send_jack_note_on("looper:control", 61, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + int still_found = 1; + for (int retries = 0; retries < 30; retries++) { + safe_usleep(100000); + ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0); + 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); + } + if (!still_found) break; + } + jack_client_close(client); + cleanup_persistent_midi_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; +} + +static int test_stop_midi(void) { + printf("Test: MIDI stop (note 65 under control key)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + return 1; + } + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_stop", 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_stop:out"); + snprintf(my_in, sizeof(my_in), "test_stop: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; + } + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.2f * 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(150000); + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(500000); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 65, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + int prev = bursts; + for (int retries = 0; retries < 20; retries++) { + safe_usleep(100000); + int cur = bursts; + if (cur == prev) break; + prev = cur; + } + int bursts_before = bursts; + safe_usleep(500000); + int bursts_after = bursts; + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (bursts_after > bursts_before + 5) { + fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n", + bursts_before, bursts_after); + return 1; + } + printf(" PASS (stop stopped playback)\n"); + return 0; +} + +static int test_midi_channel_add(void) { + printf("Test: MIDI channel creation via FIFO (add_midi)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_midi_add", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "add_midi\n", 9); + close(fd); + safe_usleep(1500000); + const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_MIDI_TYPE, 0); + int found = 0; + if (ports) { + for (int i = 0; ports[i]; i++) { + if (strstr(ports[i], "looper:channel1_midi_in")) { + found = 1; + break; + } + } + jack_free(ports); + } + jack_client_close(client); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (!found) { + fprintf(stderr, " FAIL: channel1_midi_in port not created\n"); + return 1; + } + printf(" PASS (MIDI channel created)\n"); + return 0; +} + +int test_channel(void) { + int failures = 0; + failures += test_multiple_channels(); + failures += test_control_key_modifier(); + failures += test_bind_channel(); + failures += test_bind_unbind(); + failures += test_remove_channel(); + failures += test_stop_midi(); + failures += test_midi_channel_add(); + return failures; +} diff --git a/tests/test_fifo.c b/tests/test_fifo.c new file mode 100644 index 0000000..6b67557 --- /dev/null +++ b/tests/test_fifo.c @@ -0,0 +1,160 @@ +#include "test_common.h" + +static int test_fifo_pipe(void) { + printf("Test: FIFO pipe add/remove\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_fifo", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: no JACK\n"); + return 1; + } + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "add\n", 4); + safe_usleep(1500000); + 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); + } + write(fd, "remove\n", 7); + close(fd); + safe_usleep(1500000); + 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 (!found) { + fprintf(stderr, " FAIL: channel not added via FIFO\n"); + return 1; + } + if (still_found) { + fprintf(stderr, " FAIL: channel not removed via FIFO\n"); + return 1; + } + printf(" PASS (FIFO add/remove works)\n"); + return 0; +} + +static int test_fifo_stop_bind_unbind(void) { + printf("Test: FIFO stop, bind, unbind\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + return 1; + } + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_fifo_stop", 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_fifo_stop:out"); + snprintf(my_in, sizeof(my_in), "test_fifo_stop: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; + } + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + 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(150000); + int fd = open("/tmp/looper_cmd", O_WRONLY); + if (fd < 0) { + perror("open fifo"); + jack_deactivate(client); + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + write(fd, "stop\n", 5); + write(fd, "bind 0\n", 7); + write(fd, "unbind\n", 7); + close(fd); + safe_usleep(500000); + int bursts_after = bursts; + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (bursts_after < 1) { + fprintf(stderr, " FAIL: no burst detected (probably no recording)\n"); + return 1; + } + printf(" PASS (FIFO stop, bind, unbind executed)\n"); + return 0; +} + +int test_fifo(void) { + int failures = 0; + failures += test_fifo_pipe(); + failures += test_fifo_stop_bind_unbind(); + return failures; +} diff --git a/tests/test_loop.c b/tests/test_loop.c new file mode 100644 index 0000000..d27aa34 --- /dev/null +++ b/tests/test_loop.c @@ -0,0 +1,190 @@ +#include "test_common.h" + +static int test_looper_looping(void) { + printf("Test: loop recording and playback (expect ≥3 repetitions)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + return 1; + } + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_looping", JackNoStartServer, &status); + if (!client) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " SKIP: JACK not running?\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_looping:out"); + snprintf(my_in, sizeof(my_in), "test_looping: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; + } + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(500000); + 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(150000); + safe_usleep(800000); + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(4000000); + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_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 (at least 3 repetitions)\n"); + return 0; +} + +static int test_record_loop_stop(void) { + printf("Test: full record‑loop‑stop (≥5 repetitions)\n"); + pid_t pid = start_looper(); + if (pid < 0) return 1; + if (init_persistent_midi_client() != 0) { + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n"); + return 1; + } + jack_client_t *client; + jack_status_t status; + client = jack_client_open("test_full", 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_full:out"); + snprintf(my_in, sizeof(my_in), "test_full: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; + } + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(500000); + int sr = jack_get_sample_rate(client); + continuous_sine = 0; + beep_remaining = (int)(0.5f * 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); + if (send_jack_note_on("looper:control", 1, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(2500000); + if (send_jack_note_on("looper:control", 64, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + if (send_jack_note_on("looper:control", 65, 127) != 0) { + jack_client_close(client); + kill(pid, SIGTERM); waitpid(pid, NULL, 0); + return 1; + } + safe_usleep(200000); + int total_bursts = bursts; + jack_deactivate(client); + jack_client_close(client); + cleanup_persistent_midi_client(); + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + if (total_bursts < 5) { + fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts); + return 1; + } + printf(" PASS (≥5 repetitions, stopped cleanly)\n"); + return 0; +} + +int test_loop(void) { + int failures = 0; + failures += test_looper_looping(); + failures += test_record_loop_stop(); + return failures; +}