How To Write A LADSPA Plugin?

Yesterday, I was looking for a way to add filters to my computer’s audio output and that’s when I met Linux Audio Developer’s Simple Plugin API aka LADSPA. My purpose was to do some post-processing on my computer’s audio output and it turned out the easiest way is to add a LADSPA sink to Pulseaudio.

The problem was I couldn’t find any LADSPA documentation. The ladspa.h header file and the examples in the LADSPA SDK were the only sources to figure out how to use. They’re fine sources but if I could find a step-by-step explained example code, I’d have learned faster. So that’s how the idea of the infamous Annoying Mute Line plugin came out. Note that this tutorial doesn’t need an advanced DSP knowledge but you may need to read about sampling.

LADSPA Basics

Before moving into any code, let’s look at LADSPA basics. LADSPA is a very simple API, you just need to fill in a handler function and register it, and then the LADSPA host will call it every time it has some audio samples.

By the way, I want to explain what a LADSPA host is. A LADSPA host is a program that supports LADSPA plugins. You can see some of them here. Pulseaudio and ALSA are LADSPA hosts too.

LADSPA has three main data types:

  • Descriptor: As you may understand from the name, it’s the descriptor of the plugin. It keeps handler function pointers, creator’s name, label, parameters, etc.

  • Port: Ports are parameters of our plugin. We define our ports in the descriptor.

  • Handle: This is our customized data type to track our algorithm’s state and is passed to our run() handler along with samples.

It may be hard to understand why these data types are needed but it’ll get more clear later.

Annoying Mute Line

Now, the fun part. Let’s create an annoying plugin which mutes the output audio periodically for a specified amount of time. Later we may install this plugin into our friend’s machine and watch her eat her nails out of anger. Haha.

Our plugin should take two parameters, let’s say, muteInterval and muteLength. muteInterval is the time in seconds we’ll wait before we mute the audio, and muteLength is the time in seconds we’ll mute the audio for. To understand better, let’s listen to the little part of Isaac Asimov’s speech below:

If we apply our plugin to the audio with muteInterval be 2 seconds and muteLength be 1 second, we’ll get:

So you got the idea. Now, let’s dive into the code. If you haven’t installed LADSPA SDK, you may download it from here or if you’re on Ubuntu/Debian just run:

1
apt install ladspa-sdk

Now, let’s define our handle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// State
#define MUTED 0
#define UNMUTED !MUTED

// LADSPA_Handle
typedef struct {
unsigned long sampleRate;

unsigned int muteInterval; // in seconds
unsigned int muteLength; // in seconds

LADSPA_Data *inputBuffer; // pointer to input buffer
LADSPA_Data *outputBuffer; // pointer to output buffer

unsigned long remaining; // how many samples are remained before I change the state
unsigned int state; // muted or unmuted
} AnnoyingMuteLine;

This is the struct we’ll track our code’s state. Variable names explain themselves but I just want to go over the remaining and state variable. We’ll have two states MUTED and UNMUTED. In UNMUTED state, we’ll just forward the sample we read from input buffer to output buffer. In MUTED state, we’ll ignore the sample from the input buffer and write 0.0 to the output buffer. To keep track of time, we use the remaining variable. Every time we write to the output buffer, we’ll decrement remaining by one. If remaining is zero, we’ll toggle the state variable. remaining is calculated every time we toggle the state. For MUTED state, remaining initializes as muteLength x sampleRate and for UNMUTED state, it initializes as muteInterval x sampleRate.

Since we have our handle defined, now, we can fill our handler functions. LADSPA has several handler functions named:

  • instantiate
  • connect_port
  • activate
  • run
  • run_adding
  • set_run_adding_gain
  • deactivate
  • cleanup

We won’t be using run_adding and set_run_adding_gain but you can find more information about them in ladspa.h file. Let’s start with instantiate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// handle new instance
static LADSPA_Handle instantiateMuteLine(const LADSPA_Descriptor *descriptor,
unsigned long sampleRate) {

AnnoyingMuteLine *muteLine;

muteLine = (AnnoyingMuteLine *)malloc(sizeof(AnnoyingMuteLine));

if (muteLine == NULL) {
return NULL;
}

muteLine->sampleRate = sampleRate;

return muteLine;
}

This is where we create our handle and return it to the LADSPA host. It’s like a new instance of our plugin is created. Here in the code we just set the sample rate given by our host in our handle. Note that LADSPA_Handle is of type void * so we can provide it any type we want.

connect_port handler is next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Ports
#define PORT_COUNT 4
#define ML_MUTE_INTERVAL 0
#define ML_MUTE_LENGTH 1
#define ML_INPUT 2
#define ML_OUTPUT 3

// Assign given parameters accordingly in handle
static void connectPort(LADSPA_Handle handle, unsigned long port, LADSPA_Data *data) {
AnnoyingMuteLine *muteLine = (AnnoyingMuteLine *) handle;

switch (port) {
case ML_MUTE_INTERVAL:
muteLine->muteInterval = (unsigned int)*data;
break;
case ML_MUTE_LENGTH:
muteLine->muteLength = (unsigned int)*data;
break;
case ML_INPUT:
muteLine->inputBuffer = data;
break;
case ML_OUTPUT:
muteLine->outputBuffer = data;
break;
}
}

Here we have four ports. You can think of it as we have four parameters. In this function, our host is providing us parameters so that we can set them in our handle. LADSPA says that connect_port handler will be called for each port but to support real-time changes, it may be called more than that. We’ll tell the parameter ids to our host later when we register our descriptor.

Now, let’s implement activate:

1
2
3
4
5
6
7
// initialize the state
static void activateMuteLine(LADSPA_Handle handle) {
AnnoyingMuteLine *muteLine = (AnnoyingMuteLine *)handle;

muteLine->state = MUTED;
muteLine->remaining = 0;
}

Here is the activation of our instance. We set the state as MUTED and remaining as 0 so when our run handler is called, we can immediately toggle our state since there aren’t any remaining samples for the MUTED state. The difference between activate and instantiate is that instantiate is like creating an instance and activate is called on that instance. Our host may call activate and deactivate many times after calling instantiate.

Aaand run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main handler. forward samples or mute according to state
static void runMuteLine(LADSPA_Handle handle, unsigned long sampleCount) {
AnnoyingMuteLine *muteLine = (AnnoyingMuteLine *) handle;

for (unsigned long i = 0; i < sampleCount; i++) {
if (muteLine->remaining == 0) {
muteLine->state = !muteLine->state;

if (muteLine->state == MUTED) {
muteLine->remaining = muteLine->sampleRate * muteLine->muteLength;
}
else {
muteLine->remaining = muteLine->sampleRate * muteLine->muteInterval;
}
}

if (muteLine->state == MUTED) {
muteLine->outputBuffer[i] = 0;
}
else {
muteLine->outputBuffer[i] = muteLine->inputBuffer[i];
}

muteLine->remaining--;
}
}

Here, our host provides us the sample count written to the input buffer and the handle. For every sample, we apply the logic described under the definition of AnnoyingMuteLine. Nothing more.

Last of all, let’s implement cleanup:

1
2
3
4
5
static void cleanupMuteLine(LADSPA_Handle handle) {
AnnoyingMuteLine *muteLine = (AnnoyingMuteLine *) handle;

free(handle);
}

Nothing much has been done here. We’ve just removed our handle from memory.

We’ve completed the implementations of our handler functions. Now, let’s implement the init() function and register our descriptor in it. init() will be called when our plugin is loaded into memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
static LADSPA_Descriptor *descriptor = NULL;

// On plugin load
static void __attribute__ ((constructor)) init() {
LADSPA_PortDescriptor * portDescriptors;
LADSPA_PortRangeHint * portRangeHints;
char ** portNames;

descriptor = (LADSPA_Descriptor *)malloc(sizeof(LADSPA_Descriptor));

if (!descriptor) {
return;
}

descriptor->UniqueID = 1094; // should be unique
descriptor->Label = strdup("mute_n");
descriptor->Name = strdup("Annoying Mute Line");
descriptor->Maker = strdup("paddlesteamer");
descriptor->Copyright = strdup("None");

descriptor->Properties = LADSPA_PROPERTY_HARD_RT_CAPABLE;

descriptor->PortCount = PORT_COUNT;

portDescriptors = (LADSPA_PortDescriptor *) calloc(PORT_COUNT, sizeof(LADSPA_PortDescriptor));

portDescriptors[ML_MUTE_INTERVAL] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;
portDescriptors[ML_MUTE_LENGTH] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;
portDescriptors[ML_INPUT] = LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO;
portDescriptors[ML_OUTPUT] = LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO;

descriptor->PortDescriptors = portDescriptors;

portNames = (char **) calloc(PORT_COUNT, sizeof(char *));
portNames[ML_MUTE_INTERVAL] = strdup("Interval in seconds");
portNames[ML_MUTE_LENGTH] = strdup("Mute length in seconds");
portNames[ML_INPUT] = strdup("Input");
portNames[ML_OUTPUT] = strdup("Output");

descriptor->PortNames = (const char * const *) portNames;

portRangeHints = (LADSPA_PortRangeHint *) calloc(PORT_COUNT, sizeof(LADSPA_PortRangeHint));

portRangeHints[ML_MUTE_INTERVAL].HintDescriptor = LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_DEFAULT_1;
portRangeHints[ML_MUTE_INTERVAL].LowerBound = 1;
portRangeHints[ML_MUTE_LENGTH].HintDescriptor = LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_DEFAULT_1;
portRangeHints[ML_MUTE_LENGTH].LowerBound = 1;
portRangeHints[ML_INPUT].HintDescriptor = 0;
portRangeHints[ML_OUTPUT].HintDescriptor = 0;

descriptor->PortRangeHints = portRangeHints;

descriptor->instantiate = instantiateMuteLine;
descriptor->connect_port = connectPort;
descriptor->activate = activateMuteLine;
descriptor->run = runMuteLine;
descriptor->run_adding = NULL;
descriptor->set_run_adding_gain = NULL;
descriptor->deactivate = NULL;
descriptor->cleanup = cleanupMuteLine;
}

Whew! This is a long implemention there! Let’s inspect it partially:

1
2
3
4
5
descriptor->UniqueID  = 1094; // should be unique
descriptor->Label = strdup("mute_n");
descriptor->Name = strdup("Annoying Mute Line");
descriptor->Maker = strdup("paddlesteamer");
descriptor->Copyright = strdup("None");

Here we’ve set some metadata of our plugin. Nothing interesting.

1
descriptor->Properties = LADSPA_PROPERTY_HARD_RT_CAPABLE;

We must provide LADSPA host one of the properties below:

  • LADSPA_PROPERTY_REALTIME: This means the plugin has a real-time dependency.

  • LADSPA_PROPERTY_INPLACE_BROKEN: This means the plugin may not work correctly if the input buffer and output buffer point to the same memory location.

  • LADSPA_PROPERTY_HARD_RT_CAPABLE: According to the ladspa.h, this should be provided, only if we qualify:

    (1) The plugin must not use malloc(), free() or other heap memory
    management within its run() or run_adding() functions. All new
    memory used in run() must be managed via the stack. These
    restrictions only apply to the run() function.

    (2) The plugin will not attempt to make use of any library
    functions with the exceptions of functions in the ANSI standard C
    and C maths libraries, which the host is expected to provide.

    (3) The plugin will not access files, devices, pipes, sockets, IPC
    or any other mechanism that might result in process or thread
    blocking.

    (4) The plugin will take an amount of time to execute a run() or
    run_adding() call approximately of form (A+B*SampleCount) where A
    and B depend on the machine and host in use. This amount of time
    may not depend on input signals or plugin state. The host is left
    the responsibility to perform timings to estimate upper bounds for
    A and B.

Since our plugin is a real badass and capable of hard real-time processing, we provide LADSPA_PROPERTY_HARD_RT_CAPABLE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
descriptor->PortCount = PORT_COUNT;

portDescriptors = (LADSPA_PortDescriptor *) calloc(PORT_COUNT, sizeof(LADSPA_PortDescriptor));

portDescriptors[ML_MUTE_INTERVAL] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;
portDescriptors[ML_MUTE_LENGTH] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;
portDescriptors[ML_INPUT] = LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO;
portDescriptors[ML_OUTPUT] = LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO;

descriptor->PortDescriptors = portDescriptors;

portNames = (char **) calloc(PORT_COUNT, sizeof(char *));
portNames[ML_MUTE_INTERVAL] = strdup("Interval in seconds");
portNames[ML_MUTE_LENGTH] = strdup("Mute length in seconds");
portNames[ML_INPUT] = strdup("Input");
portNames[ML_OUTPUT] = strdup("Output");

descriptor->PortNames = (const char * const *) portNames;

portRangeHints = (LADSPA_PortRangeHint *) calloc(PORT_COUNT, sizeof(LADSPA_PortRangeHint));

portRangeHints[ML_MUTE_INTERVAL].HintDescriptor = LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_DEFAULT_1;
portRangeHints[ML_MUTE_INTERVAL].LowerBound = 1;
portRangeHints[ML_MUTE_LENGTH].HintDescriptor = LADSPA_HINT_BOUNDED_BELOW | LADSPA_HINT_DEFAULT_1;
portRangeHints[ML_MUTE_LENGTH].LowerBound = 1;
portRangeHints[ML_INPUT].HintDescriptor = 0;
portRangeHints[ML_OUTPUT].HintDescriptor = 0;

descriptor->PortRangeHints = portRangeHints;

LADSPA provides four different port properties:

  • LADSPA_PORT_INPUT: Indicates port is an input.
  • LADSPA_PORT_OUTPUT: Indicates port is an ouput.
  • LADSPA_PORT_CONTROL: Indicates port is a parameter.
  • LADSPA_PORT_AUDIO: Indicates port is an audio sample buffer.

We describe our ports according to above definitions. For example, we defined:

1
portDescriptors[ML_MUTE_INTERVAL] = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL;

which means muteInterval is an input and a parameter. PortNames and PortRangeHints are self-explanatory, so I’m skipping them.

1
2
3
4
5
6
7
8
descriptor->instantiate = instantiateMuteLine;
descriptor->connect_port = connectPort;
descriptor->activate = activateMuteLine;
descriptor->run = runMuteLine;
descriptor->run_adding = NULL;
descriptor->set_run_adding_gain = NULL;
descriptor->deactivate = NULL;
descriptor->cleanup = cleanupMuteLine;

Here, we’ve just assigned our handlers to descriptors. Now let’s implement our destructor function fini():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// On plugin unload
static void __attribute__ ((destructor)) fini() {
if (descriptor == NULL) return;

free((char *) descriptor->Label);
free((char *) descriptor->Name);
free((char *) descriptor->Maker);
free((char *) descriptor->Copyright);
free((char *) descriptor->PortDescriptors);
for (int i=0; i<PORT_COUNT; i++) {
free((char *) descriptor->PortNames[i]);
}

free((char **) descriptor->PortNames);
free((LADSPA_PortRangeHint *) descriptor->PortRangeHints);
free(descriptor);
}

Since this is called on plugin unload, we should free the memory space we’ve used.

Ok, we’re almost finished:

1
2
3
4
5
6
7
8
// we only have one type of plugin
const LADSPA_Descriptor * ladspa_descriptor(unsigned long index) {
if (index != 0) {
return NULL;
}

return descriptor;
}

ladspa_descriptor is called by host to find plugin descriptors. Since we have only one plugin type, we’ll return our one and only descriptor only when the index is zero.

Now our plugin is finished. You can see the complete code at github.com/paddlesteamer/Annoying-Mute-Line. Let’s compile our code:

1
gcc -shared mute.c -o mute.so

Deploying The Prank

To complete the prank, let’s install our plugin into our friend’s LADSPA_PATH:

1
sudo cp mute.so /usr/lib/ladspa/mute.so

Note that /usr/lib/ladspa is the default LADSPA_PATH. Probably it’s same in your friend’s computer. And let’s run:

1
2
pacmd load-module module-ladspa-sink sink_name=amute plugin=mute label=mute_n control=<muteInterval>,<muteLength>
pacmd set-default-sink amute

Haha. Now her output audio will be muted periodically. Well done :///