feat: implement control key (note 64) and trigger looper command (note 62)
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
| Category | Rating | Remarks |
|
| 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 | Multi‑channel and dynamic channel add/remove are now implemented. Control key (note 64) is handled as a modifier for command selection. Backward compatibility for note 1, 60, 61 retained. |
|
||||||
| Potential Segfaults | ✅ OK | No obvious segfaults: all buffer accesses are bounds‑checked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. |
|
| Potential Segfaults | ✅ OK | No obvious segfaults: all buffer accesses are bounds‑checked (e.g., `record_pos < LOOP_BUF_SIZE`), and null pointer checks exist. |
|
||||||
| Memory Safety | ✅ OK | No dynamic memory allocation; only a fixed‑size global buffer. No leaks, no use‑after‑free. |
|
| Memory Safety | ✅ OK | No dynamic memory allocation; only a fixed‑size global buffer. No leaks, no use‑after‑free. |
|
||||||
| 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). |
|
| 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 real‑time callback. Atomic operations are cheap. Fixed buffer size (0.96 MB) is safe. |
|
| Performance | ✅ OK | Linear buffer access, no system calls or allocations in the real‑time callback. Atomic operations are cheap. Fixed buffer size (0.96 MB) is safe. |
|
||||||
| Architectural Soundness | ❌ Issue | The current design is single‑channel, static, and not extensible. A dynamic multi‑channel system is required. Global state and singular port pairs prevent scaling. No abstraction layer for channels exists. |
|
| Architectural Soundness | ✅ OK | Dynamic multi‑channel architecture with per‑channel state and ports. Real‑time safe command queue via atomic flags. Abstraction via `channel_t` struct. Extensible for future binding. |
|
||||||
|
|
||||||
## Test Evaluation
|
## Test Evaluation
|
||||||
|
|
||||||
|
|||||||
102
src/main.c
102
src/main.c
@@ -40,6 +40,9 @@ static jack_port_t *midi_control_port;
|
|||||||
static jack_port_t *midi_clock_port;
|
static jack_port_t *midi_clock_port;
|
||||||
static jack_client_t *client;
|
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 real‑time context
|
* process callback – runs in real‑time context
|
||||||
* --------------------------------------------------------------- */
|
* --------------------------------------------------------------- */
|
||||||
@@ -54,37 +57,78 @@ static int process(jack_nframes_t nframes, void *arg)
|
|||||||
jack_midi_event_t ev;
|
jack_midi_event_t ev;
|
||||||
for (jack_nframes_t i = 0; i < nevents; i++) {
|
for (jack_nframes_t i = 0; i < nevents; i++) {
|
||||||
if (jack_midi_event_get(&ev, midi_ctrl_buf, i) != 0) continue;
|
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 note = ev.buffer[1];
|
unsigned char status = ev.buffer[0];
|
||||||
switch (note) {
|
unsigned char note = ev.buffer[1];
|
||||||
case 1: /* toggle state of channel 0 (backward compatible) */
|
unsigned char vel = ev.buffer[2];
|
||||||
{
|
|
||||||
int cur0 = atomic_load(&channels[0].state);
|
/* note‑on */
|
||||||
switch (cur0) {
|
if ((status & 0xf0) == 0x90 && vel > 0) {
|
||||||
case STATE_IDLE:
|
if (note == 64) {
|
||||||
atomic_store(&channels[0].state, STATE_RECORD);
|
/* control key pressed – activate modifier */
|
||||||
break;
|
atomic_store(&control_key_active, 1);
|
||||||
case STATE_RECORD:
|
} else {
|
||||||
atomic_store(&channels[0].state, STATE_LOOPING);
|
int ck = atomic_load(&control_key_active);
|
||||||
break;
|
if (ck) {
|
||||||
case STATE_LOOPING:
|
/* command selected by control key */
|
||||||
atomic_store(&channels[0].state, STATE_PAUSED);
|
atomic_store(&control_key_active, 0);
|
||||||
break;
|
switch (note) {
|
||||||
case STATE_PAUSED:
|
case 60: atomic_store(&cmd_add, 1); break;
|
||||||
atomic_store(&channels[0].state, STATE_LOOPING);
|
case 61: atomic_store(&cmd_remove, 1); break;
|
||||||
break;
|
case 62: /* trigger looper – 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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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 if ((status & 0xf0) == 0x80 || ((status & 0xf0) == 0x90 && vel == 0)) {
|
||||||
|
/* note‑off – clear control key state */
|
||||||
|
atomic_store(&control_key_active, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user