Files
looper/engine/docs/12-command-architecture.md
2026-05-13 17:57:41 +00:00

4.3 KiB
Raw Permalink Blame History

Command Architecture

Overview

The looper uses a lockfree, singleproducer singleconsumer (SPSC) command queue to communicate between the realtime JACK audio thread and the main (nonRT) thread.
There are two families of queues:

  • cmd_queue (RTsafe) 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 50ms.

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).

RTsafe 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.

Mainthread 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 highestnumbered active dynamic channel (excluding channel0).

Command Flow

  1. MIDI input midi_handle_events parses incoming noteon events and decides which command to push.
    RTsafe 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.
    RTsafe 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 50ms). 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 twoqueue design ensures that memoryallocating operations never happen inside the RT thread, while RTpertinent commands are processed with minimal latency.