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 |
|
||||
|--------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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. |
|
||||
| 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). |
|
||||
| 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
|
||||
|
||||
|
||||
60
src/main.c
60
src/main.c
@@ -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 real‑time 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];
|
||||
|
||||
/* note‑on */
|
||||
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)) {
|
||||
/* note‑off – clear control key state */
|
||||
atomic_store(&control_key_active, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user