1-multichannel #1

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

View File

@@ -4,12 +4,12 @@
| Category | Rating | Remarks |
|--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Mocked / Left Undone | ❌ Issue | The test `test_multiple_channels` expects dynamic channel creation via MIDI note 60, but the looper does not implement this feature. Also the control key is supposed to be 64, but code uses note 1. No multiple channels. |
| 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 | ✅ OK | No obvious segfaults: all buffer accesses are boundschecked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. |
| 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 | ❌ Issue | The current design is singlechannel, static, and not extensible. A dynamic multichannel system is required. Global state and singular port pairs prevent scaling. No abstraction layer for channels exists. |
| 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

View File

@@ -40,6 +40,9 @@ static jack_port_t *midi_control_port;
static jack_port_t *midi_clock_port;
static jack_client_t *client;
/* control key mechanism note 64 acts as a modifier */
static atomic_int control_key_active = 0;
/* ---------------------------------------------------------------
* process callback runs in realtime context
* --------------------------------------------------------------- */
@@ -54,10 +57,25 @@ static int process(jack_nframes_t nframes, void *arg)
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) continue;
if ((ev.size >= 3) && ((ev.buffer[0] & 0xf0) == 0x90)) {
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) {
/* control key pressed activate modifier */
atomic_store(&control_key_active, 1);
} else {
int ck = atomic_load(&control_key_active);
if (ck) {
/* command selected by control key */
atomic_store(&control_key_active, 0);
switch (note) {
case 1: /* toggle state of channel 0 (backward compatible) */
case 60: atomic_store(&cmd_add, 1); break;
case 61: atomic_store(&cmd_remove, 1); break;
case 62: /* trigger looper toggle channel 0 */
{
int cur0 = atomic_load(&channels[0].state);
switch (cur0) {
@@ -74,17 +92,43 @@ static int process(jack_nframes_t nframes, void *arg)
atomic_store(&channels[0].state, STATE_LOOPING);
break;
}
break;
}
case 60: /* add channel send to main thread */
atomic_store(&cmd_add, 1);
break;
case 61: /* remove channel send to main thread */
atomic_store(&cmd_remove, 1);
break;
default:
break;
}
} else {
/* direct mapping (backward compatible) */
switch (note) {
case 1: /* toggle state of 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)) {
/* noteoff clear control key state */
atomic_store(&control_key_active, 0);
}
}
}