Compare commits
4 Commits
11-command
...
12-command
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4aac3356 | ||
|
|
69859a6294 | ||
|
|
d47fddbeb3 | ||
|
|
900619a714 |
0
docs/12-command-architecture
Normal file
0
docs/12-command-architecture
Normal file
65
docs/12-command-architecture.md
Normal file
65
docs/12-command-architecture.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Command Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The looper uses a **lock‑free, single‑producer single‑consumer (SPSC)** command queue to communicate between the real‑time JACK audio thread and the main (non‑RT) thread.
|
||||||
|
There are two families of queues:
|
||||||
|
|
||||||
|
- **`cmd_queue`** (RT‑safe) – used for commands that can be handled directly inside the process callback (`CMD_CYCLE`, `CMD_STOP`, `CMD_BIND_CHANNEL`, `CMD_UNBIND`).
|
||||||
|
The producer is the MIDI handler (`midi_handle_events`) or the FIFO pipe reader (`pipe_thread_func`); the consumer is `process_callback`.
|
||||||
|
|
||||||
|
- **`cmd_queue_main_midi`** / **`cmd_queue_main_fifo`** – used for commands that require memory allocation or JACK API calls (`CMD_ADD_CHANNEL`, `CMD_REMOVE_CHANNEL`).
|
||||||
|
The producer is the MIDI handler (or FIFO reader), and the consumer is `looper_process_commands`, which runs in the main loop approximately every 50 ms.
|
||||||
|
|
||||||
|
## Command Types
|
||||||
|
|
||||||
|
The `command_t` struct (defined in `command.h`) contains:
|
||||||
|
|
||||||
|
- `type` – one of the `cmd_type_t` enumerators.
|
||||||
|
- `channel` – target channel index; `-1` means “current bind channel” for some commands.
|
||||||
|
- `data` – extra parameter (e.g., bind channel number for `CMD_BIND_CHANNEL`).
|
||||||
|
|
||||||
|
### RT‑safe Commands (pushed to `cmd_queue`)
|
||||||
|
|
||||||
|
| Type | Effect |
|
||||||
|
|--------------------|---------------------------------------------------------------------|
|
||||||
|
| `CMD_CYCLE` | Toggle the state machine of the target channel (IDLE→RECORD→LOOPING→PAUSED→LOOPING…). |
|
||||||
|
| `CMD_STOP` | Force the target channel (or all channels, if `channel == -1`) to `STATE_IDLE`. |
|
||||||
|
| `CMD_BIND_CHANNEL` | Set the global `bind_channel` index to `data`. |
|
||||||
|
| `CMD_UNBIND` | Reset `bind_channel` to 0. |
|
||||||
|
|
||||||
|
### Main‑thread Commands (pushed to `cmd_queue_main_midi` / `cmd_queue_main_fifo`)
|
||||||
|
|
||||||
|
| Type | Effect |
|
||||||
|
|---------------------|---------------------------------------------------------------------|
|
||||||
|
| `CMD_ADD_CHANNEL` | Create a new dynamic channel (port registration). |
|
||||||
|
| `CMD_REMOVE_CHANNEL`| Remove the highest‑numbered active dynamic channel (excluding channel 0). |
|
||||||
|
|
||||||
|
## Command Flow
|
||||||
|
|
||||||
|
1. **MIDI input** – `midi_handle_events` parses incoming note‑on events and decides which command to push.
|
||||||
|
RT‑safe commands are pushed to `cmd_queue`; add/remove commands are pushed to `cmd_queue_main_midi`.
|
||||||
|
|
||||||
|
2. **FIFO input** – `pipe_thread_func` reads lines from `/tmp/looper_cmd` and pushes the corresponding command.
|
||||||
|
RT‑safe commands go to `cmd_queue`; add/remove go to `cmd_queue_main_fifo`.
|
||||||
|
|
||||||
|
3. **Process callback** – `process_callback` is invoked by JACK for each audio cycle. It drains `cmd_queue` and applies each command via `apply_command`. This function modifies the channel state and bind index atomically.
|
||||||
|
|
||||||
|
4. **Main loop** – `looper_process_commands` is called in the main loop (≈ every 50 ms). It drains `cmd_queue_main_midi` and `cmd_queue_main_fifo`, performing the necessary port registrations/unregistrations and calling `channel_add` / `channel_remove`.
|
||||||
|
|
||||||
|
## Deferred Port Unregistration
|
||||||
|
|
||||||
|
When a dynamic channel is removed, the RT thread first sets `active = 0`. The main thread waits until it has seen at least one full RT cycle pass (using `global_rt_cycles`) before calling `jack_port_unregister`. This prevents a race between the RT thread still holding a reference to the port buffer and the port being unregistered.
|
||||||
|
|
||||||
|
## SPSC Queue Implementation
|
||||||
|
|
||||||
|
The queue itself (defined in `queue.c`/`queue.h`) is a simple circular buffer with head and tail indices. It uses C11 atomic loads/stores with appropriate memory ordering (`memory_order_acquire`/`memory_order_release`) to guarantee visibility without locks. Capacity is fixed at `QUEUE_CAPACITY` (256 commands). Push/pop operations are O(1) and never block.
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
- The JACK process callback runs in an RT thread.
|
||||||
|
- The MIDI handler runs inside the process callback (it is called from `process_callback`).
|
||||||
|
- The FIFO reader lives in a separate POSIX thread.
|
||||||
|
- The main thread runs the rest of the program.
|
||||||
|
|
||||||
|
The two‑queue design ensures that memory‑allocating operations never happen inside the RT thread, while RT‑pertinent commands are processed with minimal latency.
|
||||||
BIN
src/channel.o
Normal file
BIN
src/channel.o
Normal file
Binary file not shown.
24
src/looper.c
24
src/looper.c
@@ -1,7 +1,9 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
|
#include "command.h"
|
||||||
#include "midi.h"
|
#include "midi.h"
|
||||||
|
#include "queue.h"
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
@@ -9,8 +11,6 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "command.h"
|
|
||||||
#include "queue.h"
|
|
||||||
|
|
||||||
/* Global state (shared across files) */
|
/* Global state (shared across files) */
|
||||||
struct channel_t channels[MAX_CHANNELS];
|
struct channel_t channels[MAX_CHANNELS];
|
||||||
@@ -36,11 +36,21 @@ static void apply_command(command_t cmd) {
|
|||||||
int cur = atomic_load(&channels[cmd.channel].state);
|
int cur = atomic_load(&channels[cmd.channel].state);
|
||||||
int next;
|
int next;
|
||||||
switch (cur) {
|
switch (cur) {
|
||||||
case STATE_IDLE: next = STATE_RECORD; break;
|
case STATE_IDLE:
|
||||||
case STATE_RECORD: next = STATE_LOOPING; break;
|
next = STATE_RECORD;
|
||||||
case STATE_LOOPING: next = STATE_PAUSED; break;
|
break;
|
||||||
case STATE_PAUSED: next = STATE_LOOPING; break;
|
case STATE_RECORD:
|
||||||
default: next = STATE_IDLE; break;
|
next = STATE_LOOPING;
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
next = STATE_PAUSED;
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
next = STATE_LOOPING;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
next = STATE_IDLE;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
atomic_store(&channels[cmd.channel].state, next);
|
atomic_store(&channels[cmd.channel].state, next);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/looper.o
Normal file
BIN
src/looper.o
Normal file
Binary file not shown.
BIN
src/main.o
Normal file
BIN
src/main.o
Normal file
Binary file not shown.
36
src/midi.c
36
src/midi.c
@@ -36,35 +36,33 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
|||||||
if (ck) {
|
if (ck) {
|
||||||
atomic_store(&control_key_active, 0);
|
atomic_store(&control_key_active, 0);
|
||||||
if (note < 16) {
|
if (note < 16) {
|
||||||
command_t cmd = { .type = CMD_BIND_CHANNEL, .channel = -1, .data = note };
|
command_t cmd = {
|
||||||
|
.type = CMD_BIND_CHANNEL, .channel = -1, .data = note};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
} else {
|
} else {
|
||||||
switch (note) {
|
switch (note) {
|
||||||
case 60:
|
case 60: {
|
||||||
{
|
command_t cmd = {
|
||||||
command_t cmd = { .type = CMD_ADD_CHANNEL, .channel = -1, .data = 0 };
|
.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
} break;
|
} break;
|
||||||
case 61:
|
case 61: {
|
||||||
{
|
command_t cmd = {
|
||||||
command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 };
|
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
} break;
|
} break;
|
||||||
case 62:
|
case 62: {
|
||||||
{
|
|
||||||
int bch = atomic_load(&bind_channel);
|
int bch = atomic_load(&bind_channel);
|
||||||
if (bch >= 0 && bch < MAX_CHANNELS) {
|
if (bch >= 0 && bch < MAX_CHANNELS) {
|
||||||
command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0};
|
command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case 63:
|
case 63: {
|
||||||
{
|
|
||||||
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
} break;
|
} break;
|
||||||
case 65:
|
case 65: {
|
||||||
{
|
|
||||||
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
} break;
|
} break;
|
||||||
@@ -75,19 +73,17 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
|||||||
} else {
|
} else {
|
||||||
/* direct mapping */
|
/* direct mapping */
|
||||||
switch (note) {
|
switch (note) {
|
||||||
case 1:
|
case 1: {
|
||||||
{
|
|
||||||
command_t cmd = {.type = CMD_CYCLE, .channel = 0, .data = 0};
|
command_t cmd = {.type = CMD_CYCLE, .channel = 0, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
} break;
|
} break;
|
||||||
case 60:
|
case 60: {
|
||||||
{
|
|
||||||
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
} break;
|
} break;
|
||||||
case 61:
|
case 61: {
|
||||||
{
|
command_t cmd = {
|
||||||
command_t cmd = { .type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0 };
|
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
queue_push(&cmd_queue_main_midi, cmd);
|
||||||
} break;
|
} break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
BIN
src/midi.o
Normal file
BIN
src/midi.o
Normal file
Binary file not shown.
10
src/pipe.c
10
src/pipe.c
@@ -1,14 +1,14 @@
|
|||||||
#include "pipe.h"
|
#include "pipe.h"
|
||||||
#include "queue.h"
|
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
#define FIFO_PATH "/tmp/looper_cmd"
|
#define FIFO_PATH "/tmp/looper_cmd"
|
||||||
#define LINE_MAX 256
|
#define LINE_MAX 256
|
||||||
|
|||||||
BIN
src/pipe.o
Normal file
BIN
src/pipe.o
Normal file
Binary file not shown.
@@ -25,6 +25,7 @@ bool queue_pop(spsc_queue_t *q, command_t *cmd) {
|
|||||||
if (t == h)
|
if (t == h)
|
||||||
return false; /* queue empty */
|
return false; /* queue empty */
|
||||||
*cmd = q->buffer[t];
|
*cmd = q->buffer[t];
|
||||||
atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY, memory_order_release);
|
atomic_store_explicit(&q->tail, (t + 1) % QUEUE_CAPACITY,
|
||||||
|
memory_order_release);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/queue.o
Normal file
BIN
src/queue.o
Normal file
Binary file not shown.
Reference in New Issue
Block a user