Louper

A journey in discovering complexity in a relatively simple software application.

Building a live looping application

I wanted to play around with a live looping pedal, but for no cost. So, what to do?

I tried some of the existing (Linux) loop recorder applications, and none of them really grabbed me as a delight to use. I had my own ideas and pre-conceptions about how it should work, maybe that got in the way of using existing software - so maybe I should just proceed to make my own?

Getting Started

By building upon the platform I made for my Guitarix Pedal:

To be honest, I hadn't really made much use of this pedal. I prefer analogue hardware for my guitar tones, and I had also perpetually put off the slightly tedious task of setting up my own presets for Guitarix.

But, underneath, it's just a Raspberry Pi running Linux. I can run other applications on it, including my own.

Application UX Prototype

To get a grip on my ideas of how this would work, I made a mock-up of what I'd want to see on screen and what the foot-switch buttons should do. In a spreadsheet.

The screen shows the following components:

The 6 buttons in yellow are annotated with their primary function, secondary function and effects of those functions.

I also noted in the spreadsheet (not shown in the screenshot) some key behaviours and unanswered questions of the application. Some of these statements are inconsistent with the terminology on the mock-up, because I changed some of the terms during the application development. Not all of these requirements and questions have yet been addressed, but those that are are ticked here:

But what do these specifications mean? How does this thing work?

Louper User Manual

The Beat Clock

There is a constantly running "beat clock" which ticks at the configured tempo and within the selected time signature. The current beat is shown prominently at the top of the display.

One of the foot-switch buttons is dedicated to tempo and time control. Short press tap for tap-tempo to set the tempo. Long-press to cycle through the preset time signatures. (Actually, none of the long press functionality is working yet).

The first press of the Tap Tempo button will also start the Metronome, which emits a short beep on the monitor (left) output channel only on each beat. The Metronome is silenced once one of the Loop Recorders enters a PLAY state.

Loop Recorders

There are also 3x loop recorder channels, which operate synchronised to the clock, but in bar intervals.

All of the 3 recorders are attached to the same audio input.

Each recorder can be in one of several states:

Once in a given state, the recorder will continue in that state unless told otherwise. However, the recorder can only change its state at the start/end of a bar - otherwise the application would require super-human timing to control.

To facilitate a smooth state change, each recorder also has a "cue" state. This cue state can be freely-selected at any time during a bar, but only becomes active at the end of the current bar. If there is no cued state, the loop recorder will continue on with its current state.

The state which will be used to "cue" a recorder is selected by pressing the "mode" button until the desired cue state is shown on screen, and then pressing the "cue" button for the desired loop recorder channel.

Generally speaking, the states are arranged such that a recorder can be "un-cued" by selecting the same cue state a 2nd time. For instance, to change your mind about entering REPLACE, you can select REPLACE again, and the recorder will not change state at the end of the bar.

You might also notice MUTE and SOLO "states" on the mode selector - these are actually not Loop Recorder states as such, they are actually Mixer states. As of today, this is implemented internally, and via the remote-control interface, but isn't possible to select with the foot-switches.

Other Internal Components

There also exists in the application a Metronome, as described above. This is fairly basic and emits short A-note beeps on the beat. There is also a Pass-through channel and an audio Mixer, so that the input can always be heard, and mixed with all the other channels before reaching the output.

The application is also running an OSC server for remote-control. The inputs from the hardware buttons are implemented as a separate process which sends messages to this server.

Make it stop!!!

There is a dedicated foot-switch button to "clear all" of the loop recorders, to make the noise stop at the end of the current bar.

System Design

I wrote this application in C++ since we require maximum performance, to aim for low latency "real-time" audio processing. It's a fairly standard cmake project outputting a library and several binaries.

There were some interesting challenges in getting the above to work though.

Loop Recorder States

It is probably apparent if you read the above "manual" that the Loop Recorder state control could easily get out of hand and not work correctly.

Each Loop Recorder actually implements two linked state machines.

The state machines are a little tricky to describe, given that the state names and events have the same names, and also that both machines also deal with the same states and events. Remember that the Loop Recorder "state" is what the recorder is currently doing. The "cue state" is what you want it to do next. The events describe what you want the "cue state" or "state" to change to now. Put another way, the "cue state" is used as an event for the next "state".

This is made worse by me now describing State Machines to change two different "states". Clear? OK, here's the state machine definitions...

State Machine 1 - Cue State

This is an interesting machine to construct. I didn't expect all these transitions and conditions when I started, but when playing with the state changes, it quickly emerges that this is required, just to make the behaviour feel intuitive and correct.

And unfortunately, I have to describe this monstrosity first, else the other state machine makes no sense.

You might notice that this can automatically cycle through many Cue States if you are inputting OVERDUB events and the recorder itself is already in a certain state; e.g. OVERDUB => REPLACE => CLEAR => REPLACE => ...

This probably isn't desirable and needs some testing and tweaking.

State Machine 2 - State

This one is a little more straightforward, it didn't require any conditions.

All of this basically just describes basic tape recorder machine mechanics, that would be familiar to anyone who used or played with a real hardware tape recorder. e.g. "You cannot play if there's no tape in the machine", "Overdubbing onto an empty tape is the same as recording (replacing) on to it".

Other Components

Most of the other components are nowhere near as complex as this, they are fairly boring and exist to support the Loop Recorders:

At least two of these deserve a bit more explanation;

Mixer

Perhaps a block diagram of the audio signal flow might explain why we need this:


  [Audio Input] ----+---- [Loop Recorder 1] ----\
                    +---- [Loop Recorder 2] -----+
                    +---- [Loop Recorder 3] -----+[Mixer] ---- [Audio Output]
                    \---- [Passthrough] ---------+
                                                 |
                          [Metronome] ----------/

Whilst it is simple to provide all the channels with the same input buffer to process (they literally all read the same buffer in turn), in order to hear all the signals at once we must add up the values from their outputs to provide to the audio output. Hence, we mix them together.

Note that the Metronome doesn't use the audio input, it generates its own signal.

Transport

Since we are wanting to operate in musical time increments of bars and beats, we need something which can tell us where we are in the music. This component exists to translate the running time of the application in to bars and beats values.

This became surprisingly tricky, given that the application is actually timed from the AudioDrv callbacks. The audio driver gives us a callback with buffers of fixed a length, which typically are a power of or multiple of two. The intervals between these callbacks depend on the sample rate and the buffer size we chose to work with when we started the system. Everything else in the application is synchronised to these callbacks.

For example, I found that the Raspberry Pi likes to operate reliably using a sample rate of 32000 samples per second and a buffer size of 96. We therefore get 1000 callbacks every 3 seconds. This happens regardless of the tempo we want to use, and never changes whilst the application is running.

Aside: it also determines our processing latency, which in this case is 3 milliseconds - low enough to use live and not notice - that also means that the all processing we do inside the callback must be completed within this time period.

What we want, however, is events to be fired inside the application exactly when a bar end or beat is occurring. e.g. to drive the Metronome, or to change the Loop Recorder channel states. These events almost never will happen in between audio callbacks, but most likely part way through, and never always in the same place. This also depends on the chosen tempo.

What the Transport does, is to count each callback and accumulate a total of the number of samples elapsed. It also knows how many samples long a bar and beat are, so for a given callback it can calculate the indices of the buffer at which these events should occur. This involves a nasty algorithm which I discovered via test-driven-development and haven't really fully analysed yet. It has to deal with the fact that the bar or beat length may be less than, the same as or greater than the audio driver buffer length - it makes no assumptions. Therefore, in any given callback, there may be zero, one, or many indices at which these events occur.

During the audio callback, we can then emit events part way through the buffer processing for bar and beat at the indices given to us by Transport, thus ensuring every state change in the application is accurately timed according to the musical time.

GUI

Yes, I implemented GUI for this, using Dear ImGui:

Its designed to fit nicely on the pedal's 800x480 screen and be readable from the floor. It could do with a couple of tweaks already, e.g. the time signature is not displayed.

The Code

The code is GPL 3 licensed and available on my bitbucket: doughammond/louper

Why "Louper" ?

For my wife, Louisa, I set this name on Valentine's day 2022 ❤️