Files
looper/docs/manual.md

17 KiB
Raw Blame History

Looper JACKbased audio looper

Overview

looper is a realtime audio and MIDI looper that runs as a JACK client. It supports multiple channels (each with multiple scenes), recording, looping, and saving/loading loops as WAV files. It can be controlled via MIDI notes or via a named FIFO (/tmp/looper_cmd).

Building

Prerequisites

  • JACK development libraries (libjack-dev or libjack-jackd2-dev)
  • libsndfile development libraries (libsndfile1-dev)
  • POSIX threads, C11 atomics
  • make

Compilation

cd engine
make

This produces the looper binary and a set of test executables.

Running

Start the JACK server if it is not already running:

jackd -d alsa -r 48000 -p 256   # example parameters

Then launch the looper:

./looper

The looper will register the following JACK ports:

  • looper:input (audio in)
  • looper:output (audio out)
  • looper:control (MIDI input control messages)
  • looper:clock (MIDI input transport clock)
  • looper:channel1_input, looper:channel1_output (first dynamic channel)

Additional ports are created for every extra channel (e.g. looper:channel2_input, …).

Architecture

  • Channels: Each channel is independent and contains up to MAX_SCENES scenes. Channel 0 always exists; additional channels can be added/removed at runtime.
  • Scenes: Each scene can be in one of four states: IDLE, RECORD, LOOPING, PAUSED. Only the current scene of a channel is active.
  • MIDI Control: Many operations are triggered by MIDI notes received on the looper:control port. A special control key (note 64) acts as a modifier: while held, subsequent notes select a different function.
  • FIFO Commands: A set of humanreadable commands can be written to /tmp/looper_cmd to control the looper from scripts or terminals.
  • Status FIFO: The looper writes its current state to /tmp/looper_status (one line per active channel).

Control

MIDI Control (port looper:control)

All MIDI notes must be on channel 0 (status byte 0x90).

Note Without modifier With modifier (hold note 64)
0 (reserved) Bind channel set the channel that will be affected by subsequent commands (note value = channel index).
1 Cycle toggles the current scene of the bound channel (or channel 0 if unbound) through IDLE→RECORD→LOOPING→PAUSED→LOOPING→…
62 Cycle same as note 1 but always acts on the bound channel.
63 Unbind resets the bound channel to channel 0.
64 Control key held while other notes are pressed to apply the modifier column.
70 Load WAV loads loop.wav from the current directory into channel 0's current scene.
71 Save WAV saves the current loop (if the scene is in LOOPING state) to save.wav.

Notes for developers: The MIDI handler is implemented in engine/src/midi.c. The controlkey state is stored in atomic_int control_key_active.

FIFO Commands (file /tmp/looper_cmd)

Write a line to the FIFO; each line activates one command.

Command line Description
record <ch> Cycle the current scene of channel <ch> (same as MIDI note 1).
stop Force all scenes of the bound channel to IDLE.
add Add a new audio channel.
add_midi Add a new MIDI channel.
remove Remove the last added dynamic channel.
bind <ch> Bind subsequent commands to channel <ch>.
unbind Unbind (revert to channel 0).
scene_add Add a new scene to the bound channel.
scene_remove Remove the current scene (not the last one) from the bound channel.
scene_next Switch to the next scene of the bound channel.
scene_prev Switch to the previous scene of the bound channel.
load Load loop.wav into channel 0's current scene.
save Save the current loop (if in LOOPING state) to save.wav.

Example session (shell)

# record something on channel 0
echo "record 0" > /tmp/looper_cmd
# after some seconds, stop recording (cycle again)
echo "record 0" > /tmp/looper_cmd
# now the loop plays back; save it
echo "save" > /tmp/looper_cmd
# add a new channel
echo "add" > /tmp/looper_cmd
# bind to that channel (assuming it is channel 1)
echo "bind 1" > /tmp/looper_cmd
# record a loop on channel 1
echo "record 1" > /tmp/looper_cmd

MIDI Clock (port looper:clock)

The looper responds to a subset of MIDI Real Time messages:

Message Action
0xFA (Start) If channel 0's current scene is IDLE, switches it to RECORD.
0xFC (Stop) Sets channel 0's current scene to IDLE.
0xFB (Continue) If channel 0's current scene is PAUSED, switches it to LOOPING.

These allow synchronisation with an external sequencer.

Detailed Behaviour

Audio Channels

  • IDLE: Input is passed through to output (live monitoring).
  • RECORD: Input is written to the loop buffer (up to LOOP_BUF_SIZE frames). The buffer is written circularly; when the buffer is full, recording overwrites from the beginning.
  • LOOPING: The recorded loop is played back repeatedly. The loop length equals the number of frames written during the last RECORD session (stored in loop_count).
  • PAUSED: Output is silent; the loop is not playing.

Transitioning from RECORD→LOOPING immediately finalises the loop length and begins playback from the start of the buffer.

MIDI Channels

MIDI channels work similarly but record MIDI events instead of audio. Received MIDI events are stored in a fixed-size array (MAX_MIDI_EVENTS). During LOOPING, all recorded events are played back at the beginning of each cycle (timestamps are not used in playback) this is suitable for drum patterns and short phrases.

Dynamic Channels

You can add and remove channels at runtime using FIFO commands or MIDI notes. Each new channel gets its own audio input/output ports and, for MIDI channels, separate MIDI ports. Removing a channel deactivates it and eventually unregisters its ports. The removal happens with a onesecond grace delay to avoid disrupting the audio thread.

Scenes

Each channel can have several scenes (default one). You can add/remove scenes and switch between them. Only the current scene is active; switching scenes preserves their individual state and loop data. This allows you to prepare multiple loops on the same channel and swap between them.

Loading WAV files

The load command reads a file named loop.wav (mono, 16bit PCM, any sample rate) from the working directory. It loads the audio into channel 0's current scene, sets the scene state to LOOPING, and begins playback. The loop length is the number of frames read (up to LOOP_BUF_SIZE).

Saving WAV files

The save command writes the current loop of channel 0's current scene to save.wav (mono, 16bit PCM, same sample rate as the JACK server). Saving is synchronous the audio thread is briefly deactivated to safely copy the buffer. The file is created in the working directory.

Status FIFO

The looper periodically writes its state to /tmp/looper_status. Each line has the format:

CH=<channel> SC=<scene_index> STATE=<IDLE|RECORD|LOOPING|PAUSED>

This can be used by external monitoring tools or scripts to track the looper's state.

Testing

Engine Tests

The engine directory contains several test executables that are built by make test. They require a running JACK server.

cd engine
make test

Tests include:

  • Audio passthrough (connectivity)
  • Loop recording and playback (counts bursts of audio)
  • Dynamic channel creation and removal
  • Controlkey modifier and channel binding
  • WAV file loading and saving

Test results are reported to stdout. If any test fails, the exit code is nonzero.

Client Tests

The client directory also contains unit and integration tests. They can be run with:

cd client
make test

These tests verify the client command parser, plugin stubs, Carla host interface, and status FIFO parsing. They do not require a running JACK server for the mockbased tests.


Client (TUI Application)

The looper project includes a terminalbased user interface client that runs alongside the engine. It provides a grid view of channels/scenes, a rack view for managing LV2/VST plugins via Carla, and a command line (:mode) for advanced operations.

Building the Client

cd client
make

This produces the looper-client binary and a test executable (test_status_parse).

Running the Client

Start the looper engine first (see Running the Engine), then launch the client:

./looper-client

The client will connect to the engine via the FIFO /tmp/looper_cmd and read status from /tmp/looper_status. It opens a ncurses interface.

TUI Keybindings

Key Action
h / Left Move selection left
j / Down Move selection down
k / Up Move selection up
l / Right Move selection right
t Toggle recording on the selected channel (sends record N)
s Switch to next scene
S Switch to previous scene
d / D Stop all scenes on the bound channel
a Add a new audio channel
A Add a new MIDI channel
r Remove the last dynamic channel
b Bind to the selected channel (sends bind N)
u Unbind (revert to channel 0)
? Toggle help overlay
R Toggle rack view (for plugin management)
Esc / Q Quit (in grid mode) or return to grid (in rack mode)
: Enter coloncommand mode (see below)

Rack View (plugin management)

When you press R, the TUI switches to a rack view showing all plugins loaded via Carla. In this mode:

Key Action
j / Down Select next plugin
k / Up Select previous plugin
b / B Bypass the selected plugin
d / D Unload the selected plugin
x / X Disconnect all JACK connections for the selected plugin
Esc Return to grid view

Colon Commands

Press : to enter commandline mode. Type a command and press Enter. The following commands are recognised:

Command Description
from <port> Store a source port name (e.g. looper:out_0)
to <port> Store a destination port name (e.g. plugin:in)
addplugin <path> Load a plugin from the given binary path. If from and to are set, it also autoconnects the plugin.
connect [from] [to] Connect two JACK ports. If omitted, uses stored from/to.
disconnect [from] [to] Disconnect two JACK ports.
rack Switch to rack view (same as R)
grid Switch to grid view

Example colon session

:from looper:channel1_output
:to system:playback_2
:addplugin /usr/lib/lv2/amsynth.lv2/amsynth.so
:connect looper:channel1_output amsynth:in

Status Display

The TUI reads the engines status FIFO (/tmp/looper_status) and displays the state of each channel as coloured cells in a 8×8 grid:

  • White (IDLE) channel is monitoring
  • Red (RECORD) channel is recording
  • Green (LOOPING) loop is playing back
  • Blue (PAUSED) loop is paused
  • Cyan currently selected cell

The status is updated continuously in real time.

Plugin Management Internals

The client uses the Carla host library to load and manage plugins. You must have Carla development libraries installed (libcarla-standalone-dev or equivalent). Plugins are loaded in a separate Carla engine instance, and their JACK ports are connected to the loopers ports using the loopers own JACK client.

The carla_host.c module wraps Carlas API and provides carla_load, carla_unload, carla_connect, etc. The plugins.c module provides a simpler interface used by the command parser.

Communication with the Engine

All actions in the TUI are translated into FIFO commands written to /tmp/looper_cmd. The engines pipe reader thread picks them up and executes them. The status FIFO /tmp/looper_status is polled by the TUIs main loop to update the display.


Configuration

The following constants can be adjusted at compile time by editing engine/src/channel.h and engine/src/looper.c:

Constant Default Description
MAX_CHANNELS 8 Maximum number of dynamic channels.
MAX_SCENES 4 Maximum scenes per channel.
LOOP_BUF_SIZE 48000 * 8 Maximum loop length in frames (8 seconds at 48 kHz).
MAX_MIDI_EVENTS 1024 Maximum MIDI events that can be recorded per scene.

Troubleshooting

  • No sound: Ensure that the JACK server is running and that you have connected looper:output to your system playback ports.
  • MIDI not working: Use jack_connect to connect your controller's output to looper:control. The note must be on MIDI channel 0.
  • save.wav not created: The scene must be in LOOPING state before issuing the save command. Also verify that a loop has been recorded (loop length > 0).
  • FIFO not working: The FIFO is created automatically. You can write to it with a simple echo "record 0" > /tmp/looper_cmd. If you get "no such file or directory", start the looper first.

Source Code Organisation

File Purpose
engine/src/main.c Entry point; opens JACK client, calls looper_init() and enters the main loop.
engine/src/looper.c Core logic: process_callback, looper_process_commands, command execution, WAV load/save.
engine/src/channel.c Channel and scene management (channel_add, channel_remove, init_scene, etc.).
engine/src/midi.c MIDI event parsing and handling for the control port.
engine/src/queue.c Lockfree SPSC queue used for passing commands between threads.
engine/src/ringbuffer.c Ring buffer used for saving loop audio to disk asynchronously (deprecated, now synchronous).
engine/src/pipe.c FIFO reader thread that translates text lines into command_t structs.
engine/src/wav.c WAV file reading and writing (libsndfile wrapper).
engine/src/command.h command_t type definition and cmd_type_t enum.
engine/src/channel.h Data structures for channel_t, scene_t, loop_data_t.
engine/src/looper.h Function prototypes for callbacks and initialisation.
engine/tests/ Integration tests for the above features.

License

[Add your license information here.]


Orchestrator and Logging

The orchestrator (orchestrator.c, built to ./looper) launches both the engine and the TUI client in a single process group. It handles graceful shutdown of both children when you press Ctrl+C.

Building

make            # builds engine, client, and orchestrator

The target orchestrator is built by the toplevel make automatically.

Running

./looper               # starts engine and client
./looper -s ~/my.rc    # loads a custom script file for the launchpad

To stop, press Ctrl+C. The orchestrator sends SIGTERM to both children and waits for them to exit.

Logging

Both the engine and the client write messages to /tmp/looper.log (appended). The file is opened at startup and closed at shutdown. The audio thread (process_callback) never calls any logging function, so realtime performance is unaffected.

To watch the log in real time:

tail -f /tmp/looper.log

Launchpad Scripting (via notes FIFO)

The client reads note events from /tmp/looper_notes (created automatically). You can feed note numbers into this FIFO using any external tool (JACK MIDItoFIFO bridge, shell script, etc.). Each line must contain a single integer note number (0127). The client then calls script_handle_note, which executes the macro associated with that note in the currently loaded script file.

The default script path is ~/.config/looper/scripts/launchpad.rc. A sample script:

mkdir -p ~/.config/looper/scripts
cat > ~/.config/looper/scripts/launchpad.rc << 'EOF'
# Grid notes 1188
11 record 0
12 record 1
13 record 2
...
# Right column (scene triggers)
19 scene_next
29 scene_next
39 scene_next
...
# Top row control keys 9198
91 stop
92 scene_prev
93 load
94 save
95 add
96 remove
97 add_midi
98 unbind
EOF

You can override the script path with the -s flag:

./looper -s /home/user/my_custom.rc

Manual generated from the looper source code v1.0.