Introduction

This is a series of well-documented example plugins that demonstrate the various features of LV2. Starting with the most basic plugin possible, each adds new functionality and explains the features used from a high level perspective.

API and vocabulary reference documentation explains details, but not the “big picture”. This book is intended to complement the reference documentation by providing good reference implementations of plugins, while also conveying a higher-level understanding of LV2.

The chapters/plugins are arranged so that each builds incrementally on its predecessor. Reading this book front to back is a good way to become familiar with modern LV2 programming. The reader is expected to be familiar with C, but otherwise no special knowledge is required; the first plugin describes the basics in detail.

This book is compiled from plugin source code into a single document for pleasant reading and ease of reference. Each chapter corresponds to executable plugin code which can be found in the plugins directory of the LV2 distribution. If you prefer to read actual source code, all the content here is also available in the source code as comments.

Simple Amplifier

This plugin is a simple example of a basic LV2 plugin with no additional features. It has audio ports which contain an array of float, and a control port which contains a single float.

LV2 plugins are defined in two parts: code and data. The code is written in C, or any C compatible language such as C++. Static data is described separately in the human and machine friendly Turtle syntax.

Generally, the goal is to keep code minimal, and describe as much as possible in the static data. There are several advantages to this approach:

  • Hosts can discover and inspect plugins without loading or executing any plugin code.

  • Plugin data can be used from a wide range of generic tools like scripting languages and command line utilities.

  • The standard data model allows the use of existing vocabularies to describe plugins and related information.

  • The language is extensible, so authors may describe any data without requiring changes to the LV2 specification.

  • Labels and documentation are translatable, and available to hosts for display in user interfaces.

manifest.ttl.in

Bundles

LV2 plugins are installed in “bundles”, a directory with a particular format. Inside the bundle, the entry point is a file called manifest.ttl. The manifest lists the plugins (or other resources) that are in this bundle, and the files that contain further information.

Hosts typically read the manifest.ttl of every bundle when starting up to discover what LV2 plugins and other resources are present. Accordingly, manifest files should be as small as possible for performance reasons.

Namespace Prefixes

Turtle files contain many URIs. To make this more readable, prefixes can be defined. For example, with the lv2: prefix below, instead of http://lv2plug.in/ns/lv2core#Plugin the shorter form lv2:Plugin can be used. This is just a shorthand for URIs within one file, the prefixes are not significant otherwise.

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

A Plugin Entry

<http://lv2plug.in/plugins/eg-amp>
        a lv2:Plugin ;
        lv2:binary <amp@LIB_EXT@>  ;
        rdfs:seeAlso <amp.ttl> .

The token @LIB_EXT@ above is replaced by the build system with the appropriate extension for the current platform (e.g. .so, .dylib, .dll). This file is called called manifest.ttl.in rather than manifest.ttl to indicate that it is not the final file to be installed. This is not necessary, but is a good idea for portable plugins. For reability, the following text will assume .so is the extension used.

In short, this declares that the resource with URI http://lv2plug.in/plugins/eg-amp is an LV2 plugin, with executable code in the file amp.so and a full description in amp.ttl. These paths are relative to the bundle directory.

There are 3 statements in this description:

Subject Predicate Object

http://lv2plug.in/plugins/eg-amp

a

lv2:Plugin

http://lv2plug.in/plugins/eg-amp

lv2:binary

<amp.so>

http://lv2plug.in/plugins/eg-amp

rdfs:seeAlso

<amp.ttl>

The semicolon is used to continue the previous subject; an equivalent but more verbose syntax for the same data is:

<http://lv2plug.in/plugins/eg-amp> a lv2:Plugin .
<http://lv2plug.in/plugins/eg-amp> lv2:binary <amp.so> .
<http://lv2plug.in/plugins/eg-amp> rdfs:seeAlso <amp.ttl> .

(Since this data is equivalent, it is safe, if pointless, to list it twice)

The documentation for a URI can often be found by visiting that URI in a web browser, e.g. the documentation for lv2:binary can be found at http://lv2plug.in/ns/lv2core#binary. All standard LV2 classes and properties are documented in this way, so if you encounter a URI in some data which you do not understand, try this first.

The URI of a plugin does not need to be a resolvable web address, it just serves as a global identifier. However, it is a good idea to use an actual web address if possible for easy access documentation, downloads, and so on, even if no documents are currently hosted there. There are compatibility rules about when the URI of a plugin must be changed, see the LV2 specification for details. Note that this does not require authors to control a top-level domain; for example, URIs in project directories at shared hosting sites are fine. It is not required to use HTTP URIs, but use of other schemes is strongly discouraged.

AUTHORS MUST NOT CREATE URIS AT DOMAINS THEY DO NOT CONTROL WITHOUT PERMISSION, AND ESPECIALLY MUST NOT CREATE INVALID URIS, E.G. WHERE THE PORTION FOLLOWING “http://” IS NOT A DOMAIN NAME. If you need an example URI, the domain http://example.org/ is reserved for this purpose.

A detailed explanation of each statement follows.

<http://lv2plug.in/plugins/eg-amp> a lv2:Plugin .

The a, as in “is a”, is a Turtle shortcut for rdf:type. lv2:Plugin expands to http://lv2plug.in/ns/lv2core#Plugin (using the lv2: prefix above) which is the type of all LV2 plugins. This statement means “http://lv2plug.in/plugins/eg-amp is an LV2 plugin”.

<http://lv2plug.in/plugins/eg-amp> lv2:binary <amp@LIB_EXT@> .

This says “eg-amp has executable code in the file amp.so”. Relative URIs in manifest files are relative to the bundle directory, so this refers to the file amp.so in the same directory as this manifest.ttl file.

<http://lv2plug.in/plugins/eg-amp> rdfs:seeAlso <amp.ttl> .

This says “there is more information about eg-amp in the file amp.ttl”. The host will look at all such files when it needs to actually use or investigate the plugin.

amp.ttl

The full description of the plugin is in this file, which is linked to from manifest.ttl. This is done so the host only needs to scan the relatively small manifest.ttl files to quickly discover all plugins.

@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .

First the type of the plugin is described. All plugins must explicitly list lv2:Plugin as a type. A more specific type should also be given, where applicable, so hosts can present a nicer UI for loading plugins. Note that this URI is the identifier of the plugin, so if it does not match the one in manifest.ttl, the host will not discover the plugin data at all.

<http://lv2plug.in/plugins/eg-amp>
        a lv2:Plugin ,
                lv2:AmplifierPlugin ;

Plugins are associated with a project, where common information like developers, home page, and so on are described. This plugin is part of the LV2 project, which has URI http://lv2plug.in/ns/lv2, and is described elsewhere. Typical plugin collections will describe the project in manifest.ttl

        lv2:project <http://lv2plug.in/ns/lv2> ;

Every plugin must have a name, described with the doap:name property. Translations to various languages can be added by putting a language tag after strings as shown.

        doap:name "Simple Amplifier" ,
                "简单放大器"@ch ,
                "Einfacher Verstärker"@de ,
                "Simple Amp"@en-gb ,
                "Amplificador Simple"@es ,
                "Amplificateur de Base"@fr ,
                "Amplificatore Semplice"@it ,
                "簡単なアンプ"@jp ,
                "Просто Усилитель"@ru ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:optionalFeature lv2:hardRTCapable ;
        lv2:port [

Every port must have at least two types, one that specifies direction (lv2:InputPort or lv2:OutputPort), and another to describe the data type. This port is a lv2:ControlPort, which means it contains a single float.

                a lv2:InputPort ,
                        lv2:ControlPort ;
                lv2:index 0 ;
                lv2:symbol "gain" ;
                lv2:name "Gain" ,
                        "收益"@ch ,
                        "Gewinn"@de ,
                        "Gain"@en-gb ,
                        "Aumento"@es ,
                        "Gain"@fr ,
                        "Guadagno"@it ,
                        "利益"@jp ,
                        "Увеличение"@ru ;

An lv2:ControlPort should always describe its default value, and usually a minimum and maximum value. Defining a range is not strictly required, but should be done wherever possible to aid host support, particularly for UIs.

                lv2:default 0.0 ;
                lv2:minimum -90.0 ;
                lv2:maximum 24.0 ;

Ports can describe units and control detents to allow better UI generation and host automation.

                units:unit units:db ;
                lv2:scalePoint [
                        rdfs:label "+5" ;
                        rdf:value 5.0
                ] , [
                        rdfs:label "0" ;
                        rdf:value 0.0
                ] , [
                        rdfs:label "-5" ;
                        rdf:value -5.0
                ] , [
                        rdfs:label "-10" ;
                        rdf:value -10.0
                ]
        ] , [
                a lv2:AudioPort ,
                        lv2:InputPort ;
                lv2:index 1 ;
                lv2:symbol "in" ;
                lv2:name "In"
        ] , [
                a lv2:AudioPort ,
                        lv2:OutputPort ;
                lv2:index 2 ;
                lv2:symbol "out" ;
                lv2:name "Out"
        ] .

amp.c

Include standard C headers

#include <math.h>
#include <stdlib.h>

LV2 headers are based on the URI of the specification they come from, so a consistent convention can be used even for unofficial extensions. The URI of the core LV2 specification is http://lv2plug.in/ns/lv2core, by replacing http:/ with lv2 any header in the specification bundle can be included, in this case lv2.h.

#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

The URI is the identifier for a plugin, and how the host associates this implementation in code with its description in data. In this plugin it is only used once in the code, but defining the plugin URI at the top of the file is a good convention to follow. If this URI does not match that used in the data files, the host will fail to load the plugin.

#define AMP_URI "http://lv2plug.in/plugins/eg-amp"

In code, ports are referred to by index. An enumeration of port indices should be defined for readability.

typedef enum {
        AMP_GAIN   = 0,
        AMP_INPUT  = 1,
        AMP_OUTPUT = 2
} PortIndex;

Every plugin defines a private structure for the plugin instance. All data associated with a plugin instance is stored here, and is available to every instance method. In this simple plugin, only port buffers need to be stored, since there is no additional instance data.

typedef struct {
        // Port buffers
        const float* gain;
        const float* input;
        float*       output;
} Amp;

The instantiate() function is called by the host to create a new plugin instance. The host passes the plugin descriptor, sample rate, and bundle path for plugins that need to load additional resources (e.g. waveforms). The features parameter contains host-provided features defined in LV2 extensions, but this simple plugin does not use any.

This function is in the “instantiation” threading class, so no other methods on this instance will be called concurrently with it.

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               bundle_path,
            const LV2_Feature* const* features)
{
        Amp* amp = (Amp*)malloc(sizeof(Amp));

        return (LV2_Handle)amp;
}

The connect_port() method is called by the host to connect a particular port to a buffer. The plugin must store the data location, but data may not be accessed except in run().

This method is in the “audio” threading class, and is called in the same context as run().

static void
connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
        Amp* amp = (Amp*)instance;

        switch ((PortIndex)port) {
        case AMP_GAIN:
                amp->gain = (const float*)data;
                break;
        case AMP_INPUT:
                amp->input = (const float*)data;
                break;
        case AMP_OUTPUT:
                amp->output = (float*)data;
                break;
        }
}

The activate() method is called by the host to initialise and prepare the plugin instance for running. The plugin must reset all internal state except for buffer locations set by connect_port(). Since this plugin has no other internal state, this method does nothing.

This method is in the “instantiation” threading class, so no other methods on this instance will be called concurrently with it.

static void
activate(LV2_Handle instance)
{
}

Define a macro for converting a gain in dB to a coefficient.

#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f)

The run() method is the main process function of the plugin. It processes a block of audio in the audio context. Since this plugin is lv2:hardRTCapable, run() must be real-time safe, so blocking (e.g. with a mutex) or memory allocation are not allowed.

static void
run(LV2_Handle instance, uint32_t n_samples)
{
        const Amp* amp = (const Amp*)instance;

        const float        gain   = *(amp->gain);
        const float* const input  = amp->input;
        float* const       output = amp->output;

        const float coef = DB_CO(gain);

        for (uint32_t pos = 0; pos < n_samples; pos++) {
                output[pos] = input[pos] * coef;
        }
}

The deactivate() method is the counterpart to activate(), and is called by the host after running the plugin. It indicates that the host will not call run() again until another call to activate() and is mainly useful for more advanced plugins with “live” characteristics such as those with auxiliary processing threads. As with activate(), this plugin has no use for this information so this method does nothing.

This method is in the “instantiation” threading class, so no other methods on this instance will be called concurrently with it.

static void
deactivate(LV2_Handle instance)
{
}

Destroy a plugin instance (counterpart to instantiate()).

This method is in the “instantiation” threading class, so no other methods on this instance will be called concurrently with it.

static void
cleanup(LV2_Handle instance)
{
        free(instance);
}

The extension_data() function returns any extension data supported by the plugin. Note that this is not an instance method, but a function on the plugin descriptor. It is usually used by plugins to implement additional interfaces. This plugin does not have any extension data, so this function returns NULL.

This method is in the “discovery” threading class, so no other functions or methods in this plugin library will be called concurrently with it.

static const void*
extension_data(const char* uri)
{
        return NULL;
}

Every plugin must define an LV2_Descriptor. It is best to define descriptors statically to avoid leaking memory and non-portable shared library constructors and destructors to clean up properly.

static const LV2_Descriptor descriptor = {
        AMP_URI,
        instantiate,
        connect_port,
        activate,
        run,
        deactivate,
        cleanup,
        extension_data
};

The lv2_descriptor() function is the entry point to the plugin library. The host will load the library and call this function repeatedly with increasing indices to find all the plugins defined in the library. The index is not an indentifier, the URI of the returned descriptor is used to determine the identify of the plugin.

This method is in the “discovery” threading class, so no other functions or methods in this plugin library will be called concurrently with it.

LV2_SYMBOL_EXPORT
const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:  return &descriptor;
        default: return NULL;
        }
}

MIDI Gate

This plugin demonstrates:

  • Receiving MIDI input

  • Processing audio based on MIDI events with sample accuracy

  • Supporting MIDI programs which the host can control/automate, or present a user interface for with human readable labels

manifest.ttl.in

The manifest.ttl file follows the same template as the previous example.

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ui:   <http://lv2plug.in/ns/extensions/ui#> .

<http://lv2plug.in/plugins/eg-midigate>
        a lv2:Plugin ;
        lv2:binary <midigate@LIB_EXT@> ;
        rdfs:seeAlso <midigate.ttl> .

midigate.ttl

The same set of namespace prefixes with two additions for LV2 extensions this plugin uses: atom and urid.

@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .

<http://lv2plug.in/plugins/eg-midigate>
        a lv2:Plugin ;
        doap:name "Example MIDI Gate" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature urid:map ;
        lv2:optionalFeature lv2:hardRTCapable ;

This plugin has three ports. There is an audio input and output as before, as well as a new AtomPort. An AtomPort buffer contains an Atom, which is a generic container for any type of data. In this case, we want to receive MIDI events, so the (mandatory) atom:bufferType is atom:Sequence, which is a series of events with time stamps.

Events themselves are also generic and can contain any type of data, but in this case we are only interested in MIDI events. The (optional) atom:supports property describes which event types are supported. Though not required, this information should always be given so the host knows what types of event it can expect the plugin to understand.

The (optional) lv2:designation of this port is lv2:control, which indicates that this is the "main" control port where the host should send events it expects to configure the plugin, in this case changing the MIDI program. This is necessary since it is possible to have several MIDI input ports, though typically it is best to have one.

        lv2:port [
                a lv2:InputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports midi:MidiEvent ;
                lv2:designation lv2:control ;
                lv2:index 0 ;
                lv2:symbol "control" ;
                lv2:name "Control"
        ] , [
                a lv2:AudioPort ,
                        lv2:InputPort ;
                lv2:index 1 ;
                lv2:symbol "in" ;
                lv2:name "In"
        ] , [
                a lv2:AudioPort ,
                        lv2:OutputPort ;
                lv2:index 2 ;
                lv2:symbol "out" ;
                lv2:name "Out"
        ] .

midigate.c

#include <stdio.h>

#include <math.h>
#include <stdlib.h>

#include "lv2/lv2plug.in/ns/ext/atom/atom.h"
#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/midi/midi.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

#define MIDIGATE_URI "http://lv2plug.in/plugins/eg-midigate"

typedef enum {
        MIDIGATE_CONTROL = 0,
        MIDIGATE_IN      = 1,
        MIDIGATE_OUT     = 2
} PortIndex;

typedef struct {
        // Port buffers
        const LV2_Atom_Sequence* control;
        const float*             in;
        float*                   out;

        // Features
        LV2_URID_Map* map;

        struct {
                LV2_URID midi_MidiEvent;
        } uris;

        unsigned n_active_notes;
        unsigned program;  // 0 = normal, 1 = inverted
} Midigate;

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               bundle_path,
            const LV2_Feature* const* features)
{

Scan features array for the URID feature we need.

        LV2_URID_Map* map = NULL;
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID__map)) {
                        map = (LV2_URID_Map*)features[i]->data;
                        break;
                }
        }
        if (!map) {

No URID feature given. This is a host bug since we require this feature, but should be handled gracefully anyway.

                return NULL;
        }

        Midigate* self = (Midigate*)calloc(1, sizeof(Midigate));
        self->map = map;
        self->uris.midi_MidiEvent = map->map(map->handle, LV2_MIDI__MidiEvent);

        return (LV2_Handle)self;
}

static void
connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
        Midigate* self = (Midigate*)instance;

        switch ((PortIndex)port) {
        case MIDIGATE_CONTROL:
                self->control = (const LV2_Atom_Sequence*)data;
                break;
        case MIDIGATE_IN:
                self->in = (const float*)data;
                break;
        case MIDIGATE_OUT:
                self->out = (float*)data;
                break;
        }
}

static void
activate(LV2_Handle instance)
{
        Midigate* self = (Midigate*)instance;
        self->n_active_notes = 0;
        self->program        = 0;
}

A function to write a chunk of output, to be called from run(). If the gate is high, then the input will be passed through for this chunk, otherwise silence is written.

static void
write_output(Midigate* self, uint32_t offset, uint32_t len)
{
        const bool active = (self->program == 0)
                ? (self->n_active_notes > 0)
                : (self->n_active_notes == 0);
        if (active) {
                memcpy(self->out + offset, self->in + offset, len * sizeof(float));
        } else {
                memset(self->out + offset, 0, len * sizeof(float));
        }
}

This plugin works through the cycle in chunks starting at offset zero. The offset represents the current time within this this cycle, so the output from 0 to offset has already been written.

MIDI events are read in a loop. In each iteration, the number of active notes (on note on and note off) or the program (on program change) is updated, then the output is written up until the current event time. Then offset is updated and the next event is processed. After the loop the final chunk from the last event to the end of the cycle is emitted.

There is currently no standard way to describe MIDI programs in LV2, so the host has no way of knowing that these programs exist and should be presented to the user. A future version of LV2 will address this shortcoming.

This pattern of iterating over input events and writing output along the way is a common idiom for writing sample accurate output based on event input.

Note that this simple example simply writes input or zero for each sample based on the gate. A serious implementation would need to envelope the transition to avoid aliasing.

static void
run(LV2_Handle instance, uint32_t sample_count)
{
        Midigate* self   = (Midigate*)instance;
        uint32_t  offset = 0;

        LV2_ATOM_SEQUENCE_FOREACH(self->control, ev) {
                if (ev->body.type == self->uris.midi_MidiEvent) {
                        const uint8_t* const msg = (const uint8_t*)(ev + 1);
                        switch (lv2_midi_message_type(msg)) {
                        case LV2_MIDI_MSG_NOTE_ON:
                                ++self->n_active_notes;
                                break;
                        case LV2_MIDI_MSG_NOTE_OFF:
                                --self->n_active_notes;
                                break;
                        case LV2_MIDI_MSG_PGM_CHANGE:
                                if (msg[1] == 0 || msg[1] == 1) {
                                        self->program = msg[1];
                                }
                                break;
                        default: break;
                        }
                }

                write_output(self, offset, ev->time.frames - offset);
                offset = (uint32_t)ev->time.frames;
        }

        write_output(self, offset, sample_count - offset);
}

We have no resources to free on deactivation. Note that the next call to activate will re-initialise the state, namely self→n_active_notes, so there is no need to do so here.

static void
deactivate(LV2_Handle instance)
{}

static void
cleanup(LV2_Handle instance)
{
        free(instance);
}

This plugin also has no extension data to return.

static const void*
extension_data(const char* uri)
{
        return NULL;
}

static const LV2_Descriptor descriptor = {
        MIDIGATE_URI,
        instantiate,
        connect_port,
        activate,
        run,
        deactivate,
        cleanup,
        extension_data
};

LV2_SYMBOL_EXPORT
const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}

Fifths

This plugin demonstrates simple MIDI event reading and writing.

manifest.ttl.in

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ui:   <http://lv2plug.in/ns/extensions/ui#> .

<http://lv2plug.in/plugins/eg-fifths>
        a lv2:Plugin ;
        lv2:binary <fifths@LIB_EXT@> ;
        rdfs:seeAlso <fifths.ttl> .

fifths.ttl

@prefix atom:  <http://lv2plug.in/ns/ext/atom#> .
@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix urid:  <http://lv2plug.in/ns/ext/urid#> .
@prefix midi:  <http://lv2plug.in/ns/ext/midi#> .

<http://lv2plug.in/plugins/eg-fifths>
        a lv2:Plugin ;
        doap:name "Example Fifths" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature urid:map ;
        lv2:optionalFeature lv2:hardRTCapable ;
        lv2:port [
                a lv2:InputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports midi:MidiEvent ;
                lv2:index 0 ;
                lv2:symbol "in" ;
                lv2:name "In"
        ] , [
                a lv2:OutputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports midi:MidiEvent ;
                lv2:index 1 ;
                lv2:symbol "out" ;
                lv2:name "Out"
        ] .

fifths.c

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef __cplusplus
#    include <stdbool.h>
#endif

#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/midi/midi.h"
#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
#include "lv2/lv2plug.in/ns/ext/state/state.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

#include "./uris.h"

enum {
        FIFTHS_IN  = 0,
        FIFTHS_OUT = 1
};

typedef struct {
        // Features
        LV2_URID_Map* map;

        // Ports
        const LV2_Atom_Sequence* in_port;
        LV2_Atom_Sequence*       out_port;

        // URIs
        FifthsURIs uris;
} Fifths;

static void
connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
        Fifths* self = (Fifths*)instance;
        switch (port) {
        case FIFTHS_IN:
                self->in_port = (const LV2_Atom_Sequence*)data;
                break;
        case FIFTHS_OUT:
                self->out_port = (LV2_Atom_Sequence*)data;
                break;
        default:
                break;
        }
}

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               path,
            const LV2_Feature* const* features)
{
        // Allocate and initialise instance structure.
        Fifths* self = (Fifths*)malloc(sizeof(Fifths));
        if (!self) {
                return NULL;
        }
        memset(self, 0, sizeof(Fifths));

        // Get host features
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID__map)) {
                        self->map = (LV2_URID_Map*)features[i]->data;
                }
        }
        if (!self->map) {
                fprintf(stderr, "Missing feature urid:map\n");
                free(self);
                return NULL;
        }

        // Map URIs and initialise forge/logger
        map_fifths_uris(self->map, &self->uris);

        return (LV2_Handle)self;
}

static void
cleanup(LV2_Handle instance)
{
        free(instance);
}

static void
run(LV2_Handle instance,
    uint32_t   sample_count)
{
        Fifths*     self = (Fifths*)instance;
        FifthsURIs* uris = &self->uris;

        // Struct for a 3 byte MIDI event, used for writing notes
        typedef struct {
                LV2_Atom_Event event;
                uint8_t        msg[3];
        } MIDINoteEvent;

        // Initially self->out_port contains a Chunk with size set to capacity

        // Get the capacity
        const uint32_t out_capacity = self->out_port->atom.size;

        // Write an empty Sequence header to the output
        lv2_atom_sequence_clear(self->out_port);
        self->out_port->atom.type = self->in_port->atom.type;

        // Read incoming events
        LV2_ATOM_SEQUENCE_FOREACH(self->in_port, ev) {
                if (ev->body.type == uris->midi_Event) {
                        const uint8_t* const msg = (const uint8_t*)(ev + 1);
                        switch (lv2_midi_message_type(msg)) {
                        case LV2_MIDI_MSG_NOTE_ON:
                        case LV2_MIDI_MSG_NOTE_OFF:
                                // Forward note to output
                                lv2_atom_sequence_append_event(
                                        self->out_port, out_capacity, ev);

                                const uint8_t note = msg[1];
                                if (note <= 127 - 7) {
                                        // Make a note one 5th (7 semitones) higher than input
                                        MIDINoteEvent fifth;

                                        // Could simply do fifth.event = *ev here instead...
                                        fifth.event.time.frames = ev->time.frames;  // Same time
                                        fifth.event.body.type   = ev->body.type;    // Same type
                                        fifth.event.body.size   = ev->body.size;    // Same size

                                        fifth.msg[0] = msg[0];      // Same status
                                        fifth.msg[1] = msg[1] + 7;  // Pitch up 7 semitones
                                        fifth.msg[2] = msg[2];      // Same velocity

                                        // Write 5th event
                                        lv2_atom_sequence_append_event(
                                                self->out_port, out_capacity, &fifth.event);
                                }
                                break;
                        default:
                                // Forward all other MIDI events directly
                                lv2_atom_sequence_append_event(
                                        self->out_port, out_capacity, ev);
                                break;
                        }
                }
        }
}

static const void*
extension_data(const char* uri)
{
        return NULL;
}

static const LV2_Descriptor descriptor = {
        EG_FIFTHS_URI,
        instantiate,
        connect_port,
        NULL,  // activate,
        run,
        NULL,  // deactivate,
        cleanup,
        extension_data
};

LV2_SYMBOL_EXPORT
const LV2_Descriptor* lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}

Metronome

This plugin demonstrates tempo synchronisation by clicking on every beat. The host sends this information to the plugin as events, so an event with new time and tempo information will be received whenever there is a change.

Time is assumed to continue rolling at the tempo and speed defined by the last received tempo event, even across cycles, until a new tempo event is received or the plugin is deactivated.

manifest.ttl.in

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<http://lv2plug.in/plugins/eg-metro>
        a lv2:Plugin ;
        lv2:binary <metro@LIB_EXT@> ;
        rdfs:seeAlso <metro.ttl> .

metro.ttl

@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix time: <http://lv2plug.in/ns/ext/time#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .

<http://lv2plug.in/plugins/eg-metro>
        a lv2:Plugin ;
        doap:name "Example Metronome" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature urid:map ;
        lv2:optionalFeature lv2:hardRTCapable ;
        lv2:port [
                a lv2:InputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;

Since this port supports time:Position, the host knows to deliver time and tempo information

                atom:supports time:Position ;
                lv2:index 0 ;
                lv2:symbol "control" ;
                lv2:name "Control" ;
        ] , [
                a lv2:AudioPort ,
                        lv2:OutputPort ;
                lv2:index 1 ;
                lv2:symbol "out" ;
                lv2:name "Out" ;
        ] .

metro.c

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef __cplusplus
#    include <stdbool.h>
#endif

#include "lv2/lv2plug.in/ns/ext/atom/atom.h"
#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/time/time.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

#ifndef M_PI
#    define M_PI 3.14159265
#endif

#define EG_METRO_URI "http://lv2plug.in/plugins/eg-metro"

typedef struct {
        LV2_URID atom_Blank;
        LV2_URID atom_Float;
        LV2_URID atom_Object;
        LV2_URID atom_Path;
        LV2_URID atom_Resource;
        LV2_URID atom_Sequence;
        LV2_URID time_Position;
        LV2_URID time_barBeat;
        LV2_URID time_beatsPerMinute;
        LV2_URID time_speed;
} MetroURIs;

static const double attack_s = 0.005;
static const double decay_s  = 0.075;

enum {
        METRO_CONTROL = 0,
        METRO_OUT     = 1
};

During execution this plugin can be in one of 3 states:

typedef enum {
        STATE_ATTACK,  // Envelope rising
        STATE_DECAY,   // Envelope lowering
        STATE_OFF      // Silent
} State;

This plugin must keep track of more state than previous examples to be able to render audio. The basic idea is to generate a single cycle of a sine wave which is conceptually played continuously. The tick is generated by enveloping the amplitude so there is a short attack/decay peak around a tick, and silence the rest of the time.

This example uses a simple AD envelope with fixed parameters. A more sophisticated implementation might use a more advanced envelope and allow the user to modify these parameters, the frequency of the wave, and so on.

typedef struct {
        LV2_URID_Map* map;   // URID map feature
        MetroURIs     uris;  // Cache of mapped URIDs

        struct {
                LV2_Atom_Sequence* control;
                float*             output;
        } ports;

        // Variables to keep track of the tempo information sent by the host
        double rate;   // Sample rate
        float  bpm;    // Beats per minute (tempo)
        float  speed;  // Transport speed (usually 0=stop, 1=play)

        uint32_t elapsed_len;  // Frames since the start of the last click
        uint32_t wave_offset;  // Current play offset in the wave
        State    state;        // Current play state

        // One cycle of a sine wave
        float*   wave;
        uint32_t wave_len;

        // Envelope parameters
        uint32_t attack_len;
        uint32_t decay_len;
} Metro;

static void
connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
        Metro* self = (Metro*)instance;

        switch (port) {
        case METRO_CONTROL:
                self->ports.control = (LV2_Atom_Sequence*)data;
                break;
        case METRO_OUT:
                self->ports.output = (float*)data;
                break;
        default:
                break;
        }
}

The activate() method resets the state completely, so the wave offset is zero and the envelope is off.

static void
activate(LV2_Handle instance)
{
        Metro* self = (Metro*)instance;

        self->elapsed_len = 0;
        self->wave_offset = 0;
        self->state       = STATE_OFF;
}

This plugin does a bit more work in instantiate() than the previous examples. The tempo updates from the host contain several URIs, so those are mapped, and the sine wave to be played needs to be generated based on the current sample rate.

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               path,
            const LV2_Feature* const* features)
{
        Metro* self = (Metro*)calloc(1, sizeof(Metro));
        if (!self) {
                return NULL;
        }

        // Scan host features for URID map
        LV2_URID_Map* map = NULL;
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) {
                        map = (LV2_URID_Map*)features[i]->data;
                }
        }
        if (!map) {
                fprintf(stderr, "Host does not support urid:map.\n");
                free(self);
                return NULL;
        }

        // Map URIS
        MetroURIs* const uris = &self->uris;
        self->map = map;
        uris->atom_Blank          = map->map(map->handle, LV2_ATOM__Blank);
        uris->atom_Float          = map->map(map->handle, LV2_ATOM__Float);
        uris->atom_Object         = map->map(map->handle, LV2_ATOM__Object);
        uris->atom_Path           = map->map(map->handle, LV2_ATOM__Path);
        uris->atom_Resource       = map->map(map->handle, LV2_ATOM__Resource);
        uris->atom_Sequence       = map->map(map->handle, LV2_ATOM__Sequence);
        uris->time_Position       = map->map(map->handle, LV2_TIME__Position);
        uris->time_barBeat        = map->map(map->handle, LV2_TIME__barBeat);
        uris->time_beatsPerMinute = map->map(map->handle, LV2_TIME__beatsPerMinute);
        uris->time_speed          = map->map(map->handle, LV2_TIME__speed);

        // Initialise instance fields
        self->rate       = rate;
        self->bpm        = 120.0f;
        self->attack_len = (uint32_t)(attack_s * rate);
        self->decay_len  = (uint32_t)(decay_s * rate);
        self->state      = STATE_OFF;

        // Generate one cycle of a sine wave at the desired frequency
        const double freq = 440.0 * 2.0;
        const double amp  = 0.5;
        self->wave_len = (uint32_t)(rate / freq);
        self->wave     = (float*)malloc(self->wave_len * sizeof(float));
        for (uint32_t i = 0; i < self->wave_len; ++i) {
                self->wave[i] = (float)(sin(i * 2 * M_PI * freq / rate) * amp);
        }

        return (LV2_Handle)self;
}

static void
cleanup(LV2_Handle instance)
{
        free(instance);
}

Play back audio for the range [begin..end) relative to this cycle. This is called by run() in-between events to output audio up until the current time.

static void
play(Metro* self, uint32_t begin, uint32_t end)
{
        float* const   output          = self->ports.output;
        const uint32_t frames_per_beat = 60.0f / self->bpm * self->rate;

        if (self->speed == 0.0f) {
                memset(output, 0, (end - begin) * sizeof(float));
                return;
        }

        for (uint32_t i = begin; i < end; ++i) {
                switch (self->state) {
                case STATE_ATTACK:
                        // Amplitude increases from 0..1 until attack_len
                        output[i] = self->wave[self->wave_offset] *
                                self->elapsed_len / (float)self->attack_len;
                        if (self->elapsed_len >= self->attack_len) {
                                self->state = STATE_DECAY;
                        }
                        break;
                case STATE_DECAY:
                        // Amplitude decreases from 1..0 until attack_len + decay_len
                        output[i] = 0.0f;
                        output[i] = self->wave[self->wave_offset] *
                                (1 - ((self->elapsed_len - self->attack_len) /
                                      (float)self->decay_len));
                        if (self->elapsed_len >= self->attack_len + self->decay_len) {
                                self->state = STATE_OFF;
                        }
                        break;
                case STATE_OFF:
                        output[i] = 0.0f;
                }

                // We continuously play the sine wave regardless of envelope
                self->wave_offset = (self->wave_offset + 1) % self->wave_len;

                // Update elapsed time and start attack if necessary
                if (++self->elapsed_len == frames_per_beat) {
                        self->state       = STATE_ATTACK;
                        self->elapsed_len = 0;
                }
        }
}

Update the current position based on a host message. This is called by run() when a time:Position is received.

static void
update_position(Metro* self, const LV2_Atom_Object* obj)
{
        const MetroURIs* uris = &self->uris;

        // Received new transport position/speed
        LV2_Atom *beat = NULL, *bpm = NULL, *speed = NULL;
        lv2_atom_object_get(obj,
                            uris->time_barBeat, &beat,
                            uris->time_beatsPerMinute, &bpm,
                            uris->time_speed, &speed,
                            NULL);
        if (bpm && bpm->type == uris->atom_Float) {
                // Tempo changed, update BPM
                self->bpm = ((LV2_Atom_Float*)bpm)->body;
        }
        if (speed && speed->type == uris->atom_Float) {
                // Speed changed, e.g. 0 (stop) to 1 (play)
                self->speed = ((LV2_Atom_Float*)speed)->body;
        }
        if (beat && beat->type == uris->atom_Float) {
                // Received a beat position, synchronise
                // This hard sync may cause clicks, a real plugin would be more graceful
                const float frames_per_beat = 60.0f / self->bpm * self->rate;
                const float bar_beats       = ((LV2_Atom_Float*)beat)->body;
                const float beat_beats      = bar_beats - floorf(bar_beats);
                self->elapsed_len           = beat_beats * frames_per_beat;
                if (self->elapsed_len < self->attack_len) {
                        self->state = STATE_ATTACK;
                } else if (self->elapsed_len < self->attack_len + self->decay_len) {
                        self->state = STATE_DECAY;
                } else {
                        self->state = STATE_OFF;
                }
        }
}

static void
run(LV2_Handle instance, uint32_t sample_count)
{
        Metro*           self = (Metro*)instance;
        const MetroURIs* uris = &self->uris;

        // Work forwards in time frame by frame, handling events as we go
        const LV2_Atom_Sequence* in     = self->ports.control;
        uint32_t                 last_t = 0;
        for (const LV2_Atom_Event* ev = lv2_atom_sequence_begin(&in->body);
             !lv2_atom_sequence_is_end(&in->body, in->atom.size, ev);
             ev = lv2_atom_sequence_next(ev)) {

                // Play the click for the time slice from last_t until now
                play(self, last_t, ev->time.frames);

                // Check if this event is an Object
                // (or deprecated Blank to tolerate old hosts)
                if (ev->body.type == uris->atom_Object ||
                    ev->body.type == uris->atom_Blank) {
                        const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body;
                        if (obj->body.otype == uris->time_Position) {
                                // Received position information, update
                                update_position(self, obj);
                        }
                }

                // Update time for next iteration and move to next event
                last_t = ev->time.frames;
        }

        // Play for remainder of cycle
        play(self, last_t, sample_count);
}

static const LV2_Descriptor descriptor = {
        EG_METRO_URI,
        instantiate,
        connect_port,
        activate,
        run,
        NULL,  // deactivate,
        cleanup,
        NULL,  // extension_data
};

LV2_SYMBOL_EXPORT const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}

Sampler

This plugin loads a single sample from a .wav file and plays it back when a MIDI note on is received. Any sample on the system can be loaded via another event. A Gtk UI is included which does this, but the host can as well.

This plugin illustrates:

  • UI ⇐⇒ Plugin communication via events

  • Use of the worker extension for non-realtime tasks (sample loading)

  • Use of the log extension to print log messages via the host

  • Saving plugin state via the state extension

  • Dynamic plugin control via the same properties saved to state

manifest.ttl.in

Unlike the previous examples, this manifest lists more than one resource: the plugin as usual, and the UI. The descriptions are similar, but have different types, so the host can decide from this file alone whether or not it is interested, and avoid following the rdfs:seeAlso link if not (though in this case both are described in the same file).

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ui:   <http://lv2plug.in/ns/extensions/ui#> .

<http://lv2plug.in/plugins/eg-sampler>
        a lv2:Plugin ;
        lv2:binary <sampler@LIB_EXT@> ;
        rdfs:seeAlso <sampler.ttl> .

<http://lv2plug.in/plugins/eg-sampler#ui>
        a ui:GtkUI ;
        ui:binary <sampler_ui@LIB_EXT@> ;
        rdfs:seeAlso <sampler.ttl> .

sampler.ttl

@prefix atom:  <http://lv2plug.in/ns/ext/atom#> .
@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix ui:    <http://lv2plug.in/ns/extensions/ui#> .
@prefix urid:  <http://lv2plug.in/ns/ext/urid#> .
@prefix work:  <http://lv2plug.in/ns/ext/worker#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .

<http://lv2plug.in/plugins/eg-sampler#sample>
        a lv2:Parameter ;
        rdfs:label "sample" ;
        rdfs:range atom:Path .

<http://lv2plug.in/plugins/eg-sampler>
        a lv2:Plugin ;
        doap:name "Example Sampler" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature urid:map ,
                work:schedule ;
        lv2:optionalFeature lv2:hardRTCapable ,
                state:loadDefaultState ;
        lv2:extensionData state:interface ,
                work:interface ;
        ui:ui <http://lv2plug.in/plugins/eg-sampler#ui> ;
        patch:writable <http://lv2plug.in/plugins/eg-sampler#sample> ;
        patch:writable param:gain ;
        lv2:port [
                a lv2:InputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ,
                        patch:Message ;
                lv2:designation lv2:control ;
                lv2:index 0 ;
                lv2:symbol "control" ;
                lv2:name "Control"
        ] , [
                a lv2:OutputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports patch:Message ;
                lv2:designation lv2:control ;
                lv2:index 1 ;
                lv2:symbol "notify" ;
                lv2:name "Notify"
        ] , [
                a lv2:AudioPort ,
                        lv2:OutputPort ;
                lv2:index 2 ;
                lv2:symbol "out" ;
                lv2:name "Out"
        ] ;
        state:state [
                <http://lv2plug.in/plugins/eg-sampler#sample> <click.wav>
        ] .

<http://lv2plug.in/plugins/eg-sampler#ui>
        a ui:GtkUI ;
        lv2:requiredFeature urid:map ;
        lv2:extensionData ui:showInterface ;
        ui:portNotification [
                ui:plugin <http://lv2plug.in/plugins/eg-sampler> ;
                lv2:symbol "notify" ;
                ui:notifyType atom:Blank
        ] .

sampler.c

#include <math.h>
#include <stdlib.h>
#include <string.h>
#ifndef __cplusplus
#    include <stdbool.h>
#endif

#include <sndfile.h>

#include "lv2/lv2plug.in/ns/ext/atom/forge.h"
#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/log/log.h"
#include "lv2/lv2plug.in/ns/ext/log/logger.h"
#include "lv2/lv2plug.in/ns/ext/midi/midi.h"
#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
#include "lv2/lv2plug.in/ns/ext/state/state.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/ext/worker/worker.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

#include "./uris.h"

enum {
        SAMPLER_CONTROL = 0,
        SAMPLER_NOTIFY  = 1,
        SAMPLER_OUT     = 2
};

static const char* default_sample_file = "click.wav";

typedef struct {
        SF_INFO  info;      // Info about sample from sndfile
        float*   data;      // Sample data in float
        char*    path;      // Path of file
        uint32_t path_len;  // Length of path
} Sample;

typedef struct {
        // Features
        LV2_URID_Map*        map;
        LV2_Worker_Schedule* schedule;
        LV2_Log_Log*         log;

        // Forge for creating atoms
        LV2_Atom_Forge forge;

        // Logger convenience API
        LV2_Log_Logger logger;

        // Sample
        Sample* sample;
        bool    sample_changed;

        // Ports
        const LV2_Atom_Sequence* control_port;
        LV2_Atom_Sequence*       notify_port;
        float*                   output_port;

        // Forge frame for notify port (for writing worker replies)
        LV2_Atom_Forge_Frame notify_frame;

        // URIs
        SamplerURIs uris;

        // Current position in run()
        uint32_t frame_offset;

        // Playback state
        float      gain;
        sf_count_t frame;
        bool       play;
} Sampler;

An atom-like message used internally to apply/free samples.

This is only used internally to communicate with the worker, it is never sent to the outside world via a port since it is not POD. It is convenient to use an Atom header so actual atoms can be easily sent through the same ringbuffer.

typedef struct {
        LV2_Atom atom;
        Sample*  sample;
} SampleMessage;

Load a new sample and return it.

Since this is of course not a real-time safe action, this is called in the worker thread only. The sample is loaded and returned only, plugin state is not modified.

static Sample*
load_sample(Sampler* self, const char* path)
{
        const size_t path_len = strlen(path);

        lv2_log_trace(&self->logger, "Loading sample %s\n", path);

        Sample* const  sample  = (Sample*)malloc(sizeof(Sample));
        SF_INFO* const info    = &sample->info;
        SNDFILE* const sndfile = sf_open(path, SFM_READ, info);

        if (!sndfile || !info->frames || (info->channels != 1)) {
                lv2_log_error(&self->logger, "Failed to open sample '%s'\n", path);
                free(sample);
                return NULL;
        }

        // Read data
        float* const data = malloc(sizeof(float) * info->frames);
        if (!data) {
                lv2_log_error(&self->logger, "Failed to allocate memory for sample\n");
                return NULL;
        }
        sf_seek(sndfile, 0ul, SEEK_SET);
        sf_read_float(sndfile, data, info->frames);
        sf_close(sndfile);

        // Fill sample struct and return it
        sample->data     = data;
        sample->path     = (char*)malloc(path_len + 1);
        sample->path_len = (uint32_t)path_len;
        memcpy(sample->path, path, path_len + 1);

        return sample;
}

static void
free_sample(Sampler* self, Sample* sample)
{
        if (sample) {
                lv2_log_trace(&self->logger, "Freeing %s\n", sample->path);
                free(sample->path);
                free(sample->data);
                free(sample);
        }
}

Do work in a non-realtime thread.

This is called for every piece of work scheduled in the audio thread using self→schedule→schedule_work(). A reply can be sent back to the audio thread using the provided respond function.

static LV2_Worker_Status
work(LV2_Handle                  instance,
     LV2_Worker_Respond_Function respond,
     LV2_Worker_Respond_Handle   handle,
     uint32_t                    size,
     const void*                 data)
{
        Sampler*        self = (Sampler*)instance;
        const LV2_Atom* atom = (const LV2_Atom*)data;
        if (atom->type == self->uris.eg_freeSample) {
                // Free old sample
                const SampleMessage* msg = (const SampleMessage*)data;
                free_sample(self, msg->sample);
        } else {
                // Handle set message (load sample).
                const LV2_Atom_Object* obj = (const LV2_Atom_Object*)data;

                // Get file path from message
                const LV2_Atom* file_path = read_set_file(&self->uris, obj);
                if (!file_path) {
                        return LV2_WORKER_ERR_UNKNOWN;
                }

                // Load sample.
                Sample* sample = load_sample(self, LV2_ATOM_BODY_CONST(file_path));
                if (sample) {
                        // Loaded sample, send it to run() to be applied.
                        respond(handle, sizeof(sample), &sample);
                }
        }

        return LV2_WORKER_SUCCESS;
}

Handle a response from work() in the audio thread.

When running normally, this will be called by the host after run(). When freewheeling, this will be called immediately at the point the work was scheduled.

static LV2_Worker_Status
work_response(LV2_Handle  instance,
              uint32_t    size,
              const void* data)
{
        Sampler* self = (Sampler*)instance;

        SampleMessage msg = { { sizeof(Sample*), self->uris.eg_freeSample },
                              self->sample };

        // Send a message to the worker to free the current sample
        self->schedule->schedule_work(self->schedule->handle, sizeof(msg), &msg);

        // Install the new sample
        self->sample = *(Sample*const*)data;

        // Send a notification that we're using a new sample.
        lv2_atom_forge_frame_time(&self->forge, self->frame_offset);
        write_set_file(&self->forge, &self->uris,
                       self->sample->path,
                       self->sample->path_len);

        return LV2_WORKER_SUCCESS;
}

static void
connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
        Sampler* self = (Sampler*)instance;
        switch (port) {
        case SAMPLER_CONTROL:
                self->control_port = (const LV2_Atom_Sequence*)data;
                break;
        case SAMPLER_NOTIFY:
                self->notify_port = (LV2_Atom_Sequence*)data;
                break;
        case SAMPLER_OUT:
                self->output_port = (float*)data;
                break;
        default:
                break;
        }
}

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               path,
            const LV2_Feature* const* features)
{
        // Allocate and initialise instance structure.
        Sampler* self = (Sampler*)malloc(sizeof(Sampler));
        if (!self) {
                return NULL;
        }
        memset(self, 0, sizeof(Sampler));

        // Get host features
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID__map)) {
                        self->map = (LV2_URID_Map*)features[i]->data;
                } else if (!strcmp(features[i]->URI, LV2_WORKER__schedule)) {
                        self->schedule = (LV2_Worker_Schedule*)features[i]->data;
                } else if (!strcmp(features[i]->URI, LV2_LOG__log)) {
                        self->log = (LV2_Log_Log*)features[i]->data;
                }
        }
        if (!self->map) {
                lv2_log_error(&self->logger, "Missing feature urid:map\n");
                goto fail;
        } else if (!self->schedule) {
                lv2_log_error(&self->logger, "Missing feature work:schedule\n");
                goto fail;
        }

        // Map URIs and initialise forge/logger
        map_sampler_uris(self->map, &self->uris);
        lv2_atom_forge_init(&self->forge, self->map);
        lv2_log_logger_init(&self->logger, self->map, self->log);

        // Load the default sample file
        const size_t path_len    = strlen(path);
        const size_t file_len    = strlen(default_sample_file);
        const size_t len         = path_len + file_len;
        char*        sample_path = (char*)malloc(len + 1);
        snprintf(sample_path, len + 1, "%s%s", path, default_sample_file);
        self->sample = load_sample(self, sample_path);
        free(sample_path);

        return (LV2_Handle)self;

fail:
        free(self);
        return 0;
}

static void
cleanup(LV2_Handle instance)
{
        Sampler* self = (Sampler*)instance;
        free_sample(self, self->sample);
        free(self);
}

Define a macro for converting a gain in dB to a coefficient.

#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f)

static void
run(LV2_Handle instance,
    uint32_t   sample_count)
{
        Sampler*     self        = (Sampler*)instance;
        SamplerURIs* uris        = &self->uris;
        sf_count_t   start_frame = 0;
        sf_count_t   pos         = 0;
        float*       output      = self->output_port;

        // Set up forge to write directly to notify output port.
        const uint32_t notify_capacity = self->notify_port->atom.size;
        lv2_atom_forge_set_buffer(&self->forge,
                                  (uint8_t*)self->notify_port,
                                  notify_capacity);

        // Start a sequence in the notify output port.
        lv2_atom_forge_sequence_head(&self->forge, &self->notify_frame, 0);

        // Send update to UI if sample has changed due to state restore
        if (self->sample_changed) {
                lv2_atom_forge_frame_time(&self->forge, 0);
                write_set_file(&self->forge, &self->uris,
                               self->sample->path,
                               self->sample->path_len);
                self->sample_changed = false;
        }

        // Read incoming events
        LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) {
                self->frame_offset = ev->time.frames;
                if (ev->body.type == uris->midi_Event) {
                        const uint8_t* const msg = (const uint8_t*)(ev + 1);
                        switch (lv2_midi_message_type(msg)) {
                        case LV2_MIDI_MSG_NOTE_ON:
                                start_frame = ev->time.frames;
                                self->frame = 0;
                                self->play  = true;
                                break;
                        default:
                                break;
                        }
                } else if (lv2_atom_forge_is_object_type(&self->forge, ev->body.type)) {
                        const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body;
                        if (obj->body.otype == uris->patch_Set) {
                                // Get the property and value of the set message
                                const LV2_Atom* property = NULL;
                                const LV2_Atom* value    = NULL;
                                lv2_atom_object_get(obj,
                                                    uris->patch_property, &property,
                                                    uris->patch_value,    &value,
                                                    0);
                                if (!property) {
                                        lv2_log_error(&self->logger,
                                                      "patch:Set message with no property\n");
                                        continue;
                                } else if (property->type != uris->atom_URID) {
                                        lv2_log_error(&self->logger,
                                                      "patch:Set property is not a URID\n");
                                        continue;
                                }

                                const uint32_t key = ((const LV2_Atom_URID*)property)->body;
                                if (key == uris->eg_sample) {
                                        // Sample change, send it to the worker.
                                        lv2_log_trace(&self->logger, "Queueing set message\n");
                                        self->schedule->schedule_work(self->schedule->handle,
                                                                      lv2_atom_total_size(&ev->body),
                                                                      &ev->body);
                                } else if (key == uris->param_gain) {
                                        // Gain change
                                        if (value->type == uris->atom_Float) {
                                                self->gain = DB_CO(((LV2_Atom_Float*)value)->body);
                                        }
                                }
                        } else if (obj->body.otype == uris->patch_Get) {
                                // Received a get message, emit our state (probably to UI)
                                lv2_log_trace(&self->logger, "Responding to get request\n");
                                lv2_atom_forge_frame_time(&self->forge, self->frame_offset);
                                write_set_file(&self->forge, &self->uris,
                                               self->sample->path,
                                               self->sample->path_len);
                        } else {
                                lv2_log_trace(&self->logger,
                                              "Unknown object type %d\n", obj->body.otype);
                        }
                } else {
                        lv2_log_trace(&self->logger,
                                      "Unknown event type %d\n", ev->body.type);
                }
        }

        // Render the sample (possibly already in progress)
        if (self->play) {
                uint32_t       f  = self->frame;
                const uint32_t lf = self->sample->info.frames;

                for (pos = 0; pos < start_frame; ++pos) {
                        output[pos] = 0;
                }

                for (; pos < sample_count && f < lf; ++pos, ++f) {
                        output[pos] = self->sample->data[f] * self->gain;
                }

                self->frame = f;

                if (f == lf) {
                        self->play = false;
                }
        }

        // Add zeros to end if sample not long enough (or not playing)
        for (; pos < sample_count; ++pos) {
                output[pos] = 0.0f;
        }
}

static LV2_State_Status
save(LV2_Handle                instance,
     LV2_State_Store_Function  store,
     LV2_State_Handle          handle,
     uint32_t                  flags,
     const LV2_Feature* const* features)
{
        Sampler* self = (Sampler*)instance;
        if (!self->sample) {
                return LV2_STATE_SUCCESS;
        }

        LV2_State_Map_Path* map_path = NULL;
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_STATE__mapPath)) {
                        map_path = (LV2_State_Map_Path*)features[i]->data;
                }
        }

        char* apath = map_path->abstract_path(map_path->handle, self->sample->path);

        store(handle,
              self->uris.eg_sample,
              apath,
              strlen(self->sample->path) + 1,
              self->uris.atom_Path,
              LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);

        free(apath);

        return LV2_STATE_SUCCESS;
}

static LV2_State_Status
restore(LV2_Handle                  instance,
        LV2_State_Retrieve_Function retrieve,
        LV2_State_Handle            handle,
        uint32_t                    flags,
        const LV2_Feature* const*   features)
{
        Sampler* self = (Sampler*)instance;

        size_t   size;
        uint32_t type;
        uint32_t valflags;

        const void* value = retrieve(
                handle,
                self->uris.eg_sample,
                &size, &type, &valflags);

        if (value) {
                const char* path = (const char*)value;
                lv2_log_trace(&self->logger, "Restoring file %s\n", path);
                free_sample(self, self->sample);
                self->sample = load_sample(self, path);
                self->sample_changed = true;
        }

        return LV2_STATE_SUCCESS;
}

static const void*
extension_data(const char* uri)
{
        static const LV2_State_Interface  state  = { save, restore };
        static const LV2_Worker_Interface worker = { work, work_response, NULL };
        if (!strcmp(uri, LV2_STATE__interface)) {
                return &state;
        } else if (!strcmp(uri, LV2_WORKER__interface)) {
                return &worker;
        }
        return NULL;
}

static const LV2_Descriptor descriptor = {
        EG_SAMPLER_URI,
        instantiate,
        connect_port,
        NULL,  // activate,
        run,
        NULL,  // deactivate,
        cleanup,
        extension_data
};

LV2_SYMBOL_EXPORT
const LV2_Descriptor* lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}

sampler_ui.c

#include <stdlib.h>

#include <gtk/gtk.h>

#include "lv2/lv2plug.in/ns/ext/atom/atom.h"
#include "lv2/lv2plug.in/ns/ext/atom/forge.h"
#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
#include "lv2/lv2plug.in/ns/extensions/ui/ui.h"

#include "./uris.h"

#define SAMPLER_UI_URI "http://lv2plug.in/plugins/eg-sampler#ui"

typedef struct {
        LV2_Atom_Forge forge;

        LV2_URID_Map* map;
        SamplerURIs   uris;

        LV2UI_Write_Function write;
        LV2UI_Controller     controller;

        GtkWidget* box;
        GtkWidget* button;
        GtkWidget* label;
        GtkWidget* window;  /* For optional show interface. */
} SamplerUI;

static void
on_load_clicked(GtkWidget* widget,
                void*      handle)
{
        SamplerUI* ui = (SamplerUI*)handle;

        /* Create a dialog to select a sample file. */
        GtkWidget* dialog = gtk_file_chooser_dialog_new(
                "Load Sample",
                NULL,
                GTK_FILE_CHOOSER_ACTION_OPEN,
                GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
                NULL);

        /* Run the dialog, and return if it is cancelled. */
        if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_ACCEPT) {
                gtk_widget_destroy(dialog);
                return;
        }

        /* Get the file path from the dialog. */
        char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));

        /* Got what we need, destroy the dialog. */
        gtk_widget_destroy(dialog);

#define OBJ_BUF_SIZE 1024
        uint8_t obj_buf[OBJ_BUF_SIZE];
        lv2_atom_forge_set_buffer(&ui->forge, obj_buf, OBJ_BUF_SIZE);

        LV2_Atom* msg = write_set_file(&ui->forge, &ui->uris,
                                       filename, strlen(filename));

        ui->write(ui->controller, 0, lv2_atom_total_size(msg),
                  ui->uris.atom_eventTransfer,
                  msg);

        g_free(filename);
}

static LV2UI_Handle
instantiate(const LV2UI_Descriptor*   descriptor,
            const char*               plugin_uri,
            const char*               bundle_path,
            LV2UI_Write_Function      write_function,
            LV2UI_Controller          controller,
            LV2UI_Widget*             widget,
            const LV2_Feature* const* features)
{
        SamplerUI* ui = (SamplerUI*)malloc(sizeof(SamplerUI));
        ui->map        = NULL;
        ui->write      = write_function;
        ui->controller = controller;
        ui->box        = NULL;
        ui->button     = NULL;
        ui->label      = NULL;
        ui->window     = NULL;

        *widget = NULL;

        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) {
                        ui->map = (LV2_URID_Map*)features[i]->data;
                }
        }

        if (!ui->map) {
                fprintf(stderr, "sampler_ui: Host does not support urid:Map\n");
                free(ui);
                return NULL;
        }

        map_sampler_uris(ui->map, &ui->uris);

        lv2_atom_forge_init(&ui->forge, ui->map);

        ui->box = gtk_vbox_new(FALSE, 4);
        ui->label = gtk_label_new("?");
        ui->button = gtk_button_new_with_label("Load Sample");
        gtk_box_pack_start(GTK_BOX(ui->box), ui->label, TRUE, TRUE, 4);
        gtk_box_pack_start(GTK_BOX(ui->box), ui->button, FALSE, FALSE, 4);
        g_signal_connect(ui->button, "clicked",
                         G_CALLBACK(on_load_clicked),
                         ui);

        // Request state (filename) from plugin
        uint8_t get_buf[512];
        lv2_atom_forge_set_buffer(&ui->forge, get_buf, sizeof(get_buf));

        LV2_Atom_Forge_Frame frame;
        LV2_Atom*            msg = (LV2_Atom*)lv2_atom_forge_object(
                &ui->forge, &frame, 0, ui->uris.patch_Get);
        lv2_atom_forge_pop(&ui->forge, &frame);

        ui->write(ui->controller, 0, lv2_atom_total_size(msg),
                  ui->uris.atom_eventTransfer,
                  msg);

        *widget = ui->box;

        return ui;
}

static void
cleanup(LV2UI_Handle handle)
{
        SamplerUI* ui = (SamplerUI*)handle;
        gtk_widget_destroy(ui->button);
        free(ui);
}

static void
port_event(LV2UI_Handle handle,
           uint32_t     port_index,
           uint32_t     buffer_size,
           uint32_t     format,
           const void*  buffer)
{
        SamplerUI* ui = (SamplerUI*)handle;
        if (format == ui->uris.atom_eventTransfer) {
                const LV2_Atom* atom = (const LV2_Atom*)buffer;
                if (lv2_atom_forge_is_object_type(&ui->forge, atom->type)) {
                        const LV2_Atom_Object* obj      = (const LV2_Atom_Object*)atom;
                        const LV2_Atom*        file_uri = read_set_file(&ui->uris, obj);
                        if (!file_uri) {
                                fprintf(stderr, "Unknown message sent to UI.\n");
                                return;
                        }

                        const char* uri = (const char*)LV2_ATOM_BODY_CONST(file_uri);
                        gtk_label_set_text(GTK_LABEL(ui->label), uri);
                } else {
                        fprintf(stderr, "Unknown message type.\n");
                }
        } else {
                fprintf(stderr, "Unknown format.\n");
        }
}

/* Optional non-embedded UI show interface. */
static int
ui_show(LV2UI_Handle handle)
{
        SamplerUI* ui = (SamplerUI*)handle;

        int argc = 0;
        gtk_init(&argc, NULL);

        ui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
        gtk_container_add(GTK_CONTAINER(ui->window), ui->box);
        gtk_widget_show_all(ui->window);
        gtk_window_present(GTK_WINDOW(ui->window));

        return 0;
}

/* Optional non-embedded UI hide interface. */
static int
ui_hide(LV2UI_Handle handle)
{
        return 0;
}

/* Idle interface for optional non-embedded UI. */
static int
ui_idle(LV2UI_Handle handle)
{
        SamplerUI* ui = (SamplerUI*)handle;
        if (ui->window) {
                gtk_main_iteration();
        }
        return 0;
}

static const void*
extension_data(const char* uri)
{
        static const LV2UI_Show_Interface show = { ui_show, ui_hide };
        static const LV2UI_Idle_Interface idle = { ui_idle };
        if (!strcmp(uri, LV2_UI__showInterface)) {
                return &show;
        } else if (!strcmp(uri, LV2_UI__idleInterface)) {
                return &idle;
        }
        return NULL;
}

static const LV2UI_Descriptor descriptor = {
        SAMPLER_UI_URI,
        instantiate,
        cleanup,
        port_event,
        extension_data
};

LV2_SYMBOL_EXPORT
const LV2UI_Descriptor*
lv2ui_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}

Simple Oscilloscope

This plugin displays the waveform of an incoming audio signal using a simple GTK+Cairo GUI.

This plugin illustrates:

  • UI ⇐⇒ Plugin communication via LV2 Atom events

  • Atom vector usage and resize-port extension

  • Save/Restore UI state by communicating state to backend

  • Saving simple key/value state via the LV2 State extension

  • Cairo drawing and partial exposure

This plugin intends to outline the basics for building visualization plugins that rely on atom communication. The UI looks like an oscilloscope, but is not a real oscilloscope implementation:

  • There is no display synchronisation, results will depend on LV2 host.

  • It displays raw audio samples, which a proper scope must not do.

  • The display itself just connects min/max line segments.

  • No triggering or synchronization.

  • No labels, no scale, no calibration, no markers, no numeric readout, etc.

Addressing these issues is beyond the scope of this example.

Please see http://lac.linuxaudio.org/2013/papers/36.pdf for scope design, https://wiki.xiph.org/Videos/Digital_Show_and_Tell for background information, and http://lists.lv2plug.in/pipermail/devel-lv2plug.in/2013-November/000545.html for general LV2 related conceptual criticism regarding real-time visualizations.

A proper oscilloscope based on this example can be found at https://github.com/x42/sisco.lv2

manifest.ttl.in

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ui:   <http://lv2plug.in/ns/extensions/ui#> .

Mono plugin variant

<http://lv2plug.in/plugins/eg-scope#Mono>
        a lv2:Plugin ;
        lv2:binary <examploscope@LIB_EXT@>  ;
        rdfs:seeAlso <examploscope.ttl> .

Stereo plugin variant

<http://lv2plug.in/plugins/eg-scope#Stereo>
        a lv2:Plugin ;
        lv2:binary <examploscope@LIB_EXT@>  ;
        rdfs:seeAlso <examploscope.ttl> .

Gtk 2.0 UI

<http://lv2plug.in/plugins/eg-scope#ui>
        a ui:GtkUI ;
        ui:binary <examploscope_ui@LIB_EXT@> ;
        rdfs:seeAlso <examploscope.ttl> .

examploscope.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include "lv2/lv2plug.in/ns/ext/log/log.h"
#include "lv2/lv2plug.in/ns/ext/log/logger.h"
#include "lv2/lv2plug.in/ns/ext/state/state.h"
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

#include "./uris.h"

Private Plugin Instance Structure

In addition to the usual port buffers and features, this plugin stores the state of the UI here, so it can be opened and closed without losing the current settings. The UI state is communicated between the plugin and the UI using atom messages via a sequence port, similarly to MIDI I/O.

typedef struct {
        // Port buffers
        float*                   input[2];
        float*                   output[2];
        const LV2_Atom_Sequence* control;
        LV2_Atom_Sequence*       notify;

        // Atom forge and URI mapping
        LV2_URID_Map*        map;
        ScoLV2URIs           uris;
        LV2_Atom_Forge       forge;
        LV2_Atom_Forge_Frame frame;

        // Log feature and convenience API
        LV2_Log_Log*   log;
        LV2_Log_Logger logger;

        // Instantiation settings
        uint32_t n_channels;
        double   rate;

        // UI state
        bool     ui_active;
        bool     send_settings_to_ui;
        float    ui_amp;
        uint32_t ui_spp;
} EgScope;

Port Indices

typedef enum {
        SCO_CONTROL = 0,  // Event input
        SCO_NOTIFY  = 1,  // Event output
        SCO_INPUT0  = 2,  // Audio input 0
        SCO_OUTPUT0 = 3,  // Audio output 0
        SCO_INPUT1  = 4,  // Audio input 1 (stereo variant)
        SCO_OUTPUT1 = 5,  // Audio input 2 (stereo variant)
} PortIndex;

Instantiate Method

static LV2_Handle
instantiate(const LV2_Descriptor*     descriptor,
            double                    rate,
            const char*               bundle_path,
            const LV2_Feature* const* features)
{
        (void)descriptor;   // Unused variable
        (void)bundle_path;  // Unused variable

        // Allocate and initialise instance structure.
        EgScope* self = (EgScope*)calloc(1, sizeof(EgScope));
        if (!self) {
                return NULL;
        }

        // Get host features
        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID__map)) {
                        self->map = (LV2_URID_Map*)features[i]->data;
                } else if (!strcmp(features[i]->URI, LV2_LOG__log)) {
                        self->log = (LV2_Log_Log*)features[i]->data;
                }
        }

        if (!self->map) {
                fprintf(stderr, "EgScope.lv2 error: Host does not support urid:map\n");
                free(self);
                return NULL;
        }

        // Decide which variant to use depending on the plugin URI
        if (!strcmp(descriptor->URI, SCO_URI "#Stereo")) {
                self->n_channels = 2;
        } else if (!strcmp(descriptor->URI, SCO_URI "#Mono")) {
                self->n_channels = 1;
        } else {
                free(self);
                return NULL;
        }

        // Initialise local variables
        self->ui_active           = false;
        self->send_settings_to_ui = false;
        self->rate                = rate;

        // Set default UI settings
        self->ui_spp = 50;
        self->ui_amp = 1.0;

        // Map URIs and initialise forge/logger
        map_sco_uris(self->map, &self->uris);
        lv2_atom_forge_init(&self->forge, self->map);
        lv2_log_logger_init(&self->logger, self->map, self->log);

        return (LV2_Handle)self;
}

Connect Port Method

static void
connect_port(LV2_Handle handle,
             uint32_t   port,
             void*      data)
{
        EgScope* self = (EgScope*)handle;

        switch ((PortIndex)port) {
        case SCO_CONTROL:
                self->control = (const LV2_Atom_Sequence*)data;
                break;
        case SCO_NOTIFY:
                self->notify = (LV2_Atom_Sequence*)data;
                break;
        case SCO_INPUT0:
                self->input[0] = (float*)data;
                break;
        case SCO_OUTPUT0:
                self->output[0] = (float*)data;
                break;
        case SCO_INPUT1:
                self->input[1] = (float*)data;
                break;
        case SCO_OUTPUT1:
                self->output[1] = (float*)data;
                break;
        }
}

Utility Function: tx_rawaudio

This function forges a message for sending a vector of raw data. The object is a Blank with a few properties, like:

[]
        a sco:RawAudio ;
        sco:channelID 0 ;
        sco:audioData [ 0.0, 0.0, ... ] .

where the value of the sco:audioData property, [ 0.0, 0.0, ... ], is a Vector of Float.

static void
tx_rawaudio(LV2_Atom_Forge* forge,
            ScoLV2URIs*     uris,
            const int32_t   channel,
            const size_t    n_samples,
            const float*    data)
{
        LV2_Atom_Forge_Frame frame;

        // Forge container object of type 'RawAudio'
        lv2_atom_forge_frame_time(forge, 0);
        lv2_atom_forge_object(forge, &frame, 0, uris->RawAudio);

        // Add integer 'channelID' property
        lv2_atom_forge_key(forge, uris->channelID);
        lv2_atom_forge_int(forge, channel);

        // Add vector of floats 'audioData' property
        lv2_atom_forge_key(forge, uris->audioData);
        lv2_atom_forge_vector(
                forge, sizeof(float), uris->atom_Float, n_samples, data);

        // Close off object
        lv2_atom_forge_pop(forge, &frame);
}

Run Method

static void
run(LV2_Handle handle, uint32_t n_samples)
{
        EgScope* self = (EgScope*)handle;

        /* Ensure notify port buffer is large enough to hold all audio-samples and
           configuration settings.  A minimum size was requested in the .ttl file,
           but check here just to be sure.

           TODO: Explain these magic numbers.
        */
        const size_t   size  = (sizeof(float) * n_samples + 64) * self->n_channels;
        const uint32_t space = self->notify->atom.size;
        if (space < size + 128) {
                /* Insufficient space, report error and do nothing.  Note that a
                   real-time production plugin mustn't call log functions in run(), but
                   this can be useful for debugging and example purposes.
                */
                lv2_log_error(&self->logger, "Buffer size is insufficient\n");
                return;
        }

        // Prepare forge buffer and initialize atom-sequence
        lv2_atom_forge_set_buffer(&self->forge, (uint8_t*)self->notify, space);
        lv2_atom_forge_sequence_head(&self->forge, &self->frame, 0);

        /* Send settings to UI

           The plugin can continue to run while the UI is closed and re-opened.
           The state and settings of the UI are kept here and transmitted to the UI
           every time it asks for them or if the user initializes a 'load preset'.
        */
        if (self->send_settings_to_ui && self->ui_active) {
                self->send_settings_to_ui = false;
                // Forge container object of type 'ui_state'
                LV2_Atom_Forge_Frame frame;
                lv2_atom_forge_frame_time(&self->forge, 0);
                lv2_atom_forge_object(&self->forge, &frame, 0, self->uris.ui_State);

                // Add UI state as properties
                lv2_atom_forge_key(&self->forge, self->uris.ui_spp);
                lv2_atom_forge_int(&self->forge, self->ui_spp);
                lv2_atom_forge_key(&self->forge, self->uris.ui_amp);
                lv2_atom_forge_float(&self->forge, self->ui_amp);
                lv2_atom_forge_key(&self->forge, self->uris.param_sampleRate);
                lv2_atom_forge_float(&self->forge, self->rate);
                lv2_atom_forge_pop(&self->forge, &frame);
        }

        // Process incoming events from GUI
        if (self->control) {
                const LV2_Atom_Event* ev = lv2_atom_sequence_begin(
                        &(self->control)->body);
                // For each incoming message...
                while (!lv2_atom_sequence_is_end(
                               &self->control->body, self->control->atom.size, ev)) {
                        // If the event is an atom:Blank object
                        if (ev->body.type == self->uris.atom_Blank) {
                                const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body;
                                if (obj->body.otype == self->uris.ui_On) {
                                        // If the object is a ui-on, the UI was activated
                                        self->ui_active           = true;
                                        self->send_settings_to_ui = true;
                                } else if (obj->body.otype == self->uris.ui_Off) {
                                        // If the object is a ui-off, the UI was closed
                                        self->ui_active = false;
                                } else if (obj->body.otype == self->uris.ui_State) {
                                        // If the object is a ui-state, it's the current UI settings
                                        const LV2_Atom* spp = NULL;
                                        const LV2_Atom* amp = NULL;
                                        lv2_atom_object_get(obj, self->uris.ui_spp, &spp,
                                                            self->uris.ui_amp, &amp,
                                                            0);
                                        if (spp) {
                                                self->ui_spp = ((const LV2_Atom_Int*)spp)->body;
                                        }
                                        if (amp) {
                                                self->ui_amp = ((const LV2_Atom_Float*)amp)->body;
                                        }
                                }
                        }
                        ev = lv2_atom_sequence_next(ev);
                }
        }

        // Process audio data
        for (uint32_t c = 0; c < self->n_channels; ++c) {
                if (self->ui_active) {
                        // If UI is active, send raw audio data to UI
                        tx_rawaudio(&self->forge, &self->uris, c, n_samples, self->input[c]);
                }
                // If not processing audio in-place, forward audio
                if (self->input[c] != self->output[c]) {
                        memcpy(self->output[c], self->input[c], sizeof(float) * n_samples);
                }
        }

        // Close off sequence
        lv2_atom_forge_pop(&self->forge, &self->frame);
}

static void
cleanup(LV2_Handle handle)
{
        free(handle);
}

State Methods

This plugin’s state consists of two basic properties: one int and one float. No files are used. Note these values are POD, but not portable, since different machines may have a different integer endianness or floating point format. However, since standard Atom types are used, a good host will be able to save them portably as text anyway.

static LV2_State_Status
state_save(LV2_Handle                instance,
           LV2_State_Store_Function  store,
           LV2_State_Handle          handle,
           uint32_t                  flags,
           const LV2_Feature* const* features)
{
        EgScope* self = (EgScope*)instance;
        if (!self) {
                return LV2_STATE_SUCCESS;
        }

        store(handle, self->uris.ui_spp,
              (void*)&self->ui_spp, sizeof(uint32_t),
              self->uris.atom_Int,
              LV2_STATE_IS_POD);

        store(handle, self->uris.ui_amp,
              (void*)&self->ui_amp, sizeof(float),
              self->uris.atom_Float,
              LV2_STATE_IS_POD);

        return LV2_STATE_SUCCESS;
}

static LV2_State_Status
state_restore(LV2_Handle                  instance,
              LV2_State_Retrieve_Function retrieve,
              LV2_State_Handle            handle,
              uint32_t                    flags,
              const LV2_Feature* const*   features)
{
        EgScope* self = (EgScope*)instance;

        size_t   size;
        uint32_t type;
        uint32_t valflags;

        const void* spp = retrieve(
                handle, self->uris.ui_spp, &size, &type, &valflags);
        if (spp && size == sizeof(uint32_t) && type == self->uris.atom_Int) {
                self->ui_spp              = *((const uint32_t*)spp);
                self->send_settings_to_ui = true;
        }

        const void* amp = retrieve(
                handle, self->uris.ui_amp, &size, &type, &valflags);
        if (amp && size == sizeof(float) && type == self->uris.atom_Float) {
                self->ui_amp              = *((const float*)amp);
                self->send_settings_to_ui = true;
        }

        return LV2_STATE_SUCCESS;
}

static const void*
extension_data(const char* uri)
{
        static const LV2_State_Interface state = { state_save, state_restore };
        if (!strcmp(uri, LV2_STATE__interface)) {
                return &state;
        }
        return NULL;
}

Plugin Descriptors

static const LV2_Descriptor descriptor_mono = {
        SCO_URI "#Mono",
        instantiate,
        connect_port,
        NULL,
        run,
        NULL,
        cleanup,
        extension_data
};

static const LV2_Descriptor descriptor_stereo = {
        SCO_URI "#Stereo",
        instantiate,
        connect_port,
        NULL,
        run,
        NULL,
        cleanup,
        extension_data
};

LV2_SYMBOL_EXPORT
const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor_mono;
        case 1:
                return &descriptor_stereo;
        default:
                return NULL;
        }
}

examploscope_ui.c

#include <stdio.h>
#include <stdlib.h>

#include <cairo.h>
#include <gtk/gtk.h>

#include "lv2/lv2plug.in/ns/extensions/ui/ui.h"
#include "./uris.h"

// Drawing area size
#define DAWIDTH  (640)
#define DAHEIGHT (200)

Max continuous points on path. Many short-path segments are expensive|inefficient long paths are not supported by all surfaces (usually its a miter - not point - limit, depending on used cairo backend)

#define MAX_CAIRO_PATH (128)

Representation of the raw audio-data for display (min | max) values for a given index position.

typedef struct {
        float data_min[DAWIDTH];
        float data_max[DAWIDTH];

        uint32_t idx;
        uint32_t sub;
} ScoChan;

typedef struct {
        LV2_Atom_Forge forge;
        LV2_URID_Map*  map;
        ScoLV2URIs     uris;

        LV2UI_Write_Function write;
        LV2UI_Controller     controller;

        GtkWidget*     hbox;
        GtkWidget*     vbox;
        GtkWidget*     sep[2];
        GtkWidget*     darea;
        GtkWidget*     btn_pause;
        GtkWidget*     lbl_speed;
        GtkWidget*     lbl_amp;
        GtkWidget*     spb_speed;
        GtkWidget*     spb_amp;
        GtkAdjustment* spb_speed_adj;
        GtkAdjustment* spb_amp_adj;

        ScoChan  chn[2];
        uint32_t stride;
        uint32_t n_channels;
        bool     paused;
        float    rate;
} EgScopeUI;

Send current UI settings to backend.

static void
send_ui_state(LV2UI_Handle handle)
{
        EgScopeUI*  ui   = (EgScopeUI*)handle;
        const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp));

        // Use local buffer on the stack to build atom
        uint8_t obj_buf[1024];
        lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));

        // Start a ui:State object
        LV2_Atom_Forge_Frame frame;
        LV2_Atom*            msg = (LV2_Atom*)lv2_atom_forge_object(
                &ui->forge, &frame, 0, ui->uris.ui_State);

        // msg[samples-per-pixel] = integer
        lv2_atom_forge_key(&ui->forge, ui->uris.ui_spp);
        lv2_atom_forge_int(&ui->forge, ui->stride);

        // msg[amplitude] = float
        lv2_atom_forge_key(&ui->forge, ui->uris.ui_amp);
        lv2_atom_forge_float(&ui->forge, gain);

        // Finish ui:State object
        lv2_atom_forge_pop(&ui->forge, &frame);

        // Send message to plugin port '0'
        ui->write(ui->controller,
                  0,
                  lv2_atom_total_size(msg),
                  ui->uris.atom_eventTransfer,
                  msg);
}

Notify backend that UI is closed.

static void
send_ui_disable(LV2UI_Handle handle)
{
        EgScopeUI* ui = (EgScopeUI*)handle;
        send_ui_state(handle);

        uint8_t obj_buf[64];
        lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));

        LV2_Atom_Forge_Frame frame;
        LV2_Atom*            msg = (LV2_Atom*)lv2_atom_forge_object(
                &ui->forge, &frame, 0, ui->uris.ui_Off);
        lv2_atom_forge_pop(&ui->forge, &frame);
        ui->write(ui->controller,
                  0,
                  lv2_atom_total_size(msg),
                  ui->uris.atom_eventTransfer,
                  msg);
}

Notify backend that UI is active.

The plugin should send state and enable data transmission.

static void
send_ui_enable(LV2UI_Handle handle)
{
        EgScopeUI* ui = (EgScopeUI*)handle;

        uint8_t obj_buf[64];
        lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));

        LV2_Atom_Forge_Frame frame;
        LV2_Atom*            msg = (LV2_Atom*)lv2_atom_forge_object(
                &ui->forge, &frame, 0, ui->uris.ui_On);
        lv2_atom_forge_pop(&ui->forge, &frame);
        ui->write(ui->controller,
                  0,
                  lv2_atom_total_size(msg),
                  ui->uris.atom_eventTransfer,
                  msg);
}

Gtk widget callback.

static gboolean
on_cfg_changed(GtkWidget* widget, gpointer data)
{
        send_ui_state(data);
        return TRUE;
}

Gdk drawing area draw callback.

Called in Gtk’s main thread and uses Cairo to draw the data.

static gboolean
on_expose_event(GtkWidget* widget, GdkEventExpose* ev, gpointer data)
{
        EgScopeUI*  ui   = (EgScopeUI*)data;
        const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp));

        // Get cairo type for the gtk window
        cairo_t* cr;
        cr = gdk_cairo_create(ui->darea->window);

        // Limit cairo-drawing to exposed area
        cairo_rectangle(cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
        cairo_clip(cr);

        // Clear background
        cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
        cairo_rectangle(cr, 0, 0, DAWIDTH, DAHEIGHT * ui->n_channels);
        cairo_fill(cr);

        cairo_set_line_width(cr, 1.0);

        const uint32_t start = ev->area.x;
        const uint32_t end   = ev->area.x + ev->area.width;

        assert(start < DAWIDTH);
        assert(end <= DAWIDTH);
        assert(start < end);

        for (uint32_t c = 0; c < ui->n_channels; ++c) {
                ScoChan* chn = &ui->chn[c];

                /* Drawing area Y-position of given sample-value.
                 * Note: cairo-pixel at 0 spans -0.5 .. +0.5, hence (DAHEIGHT / 2.0 -0.5)
                 * also the cairo Y-axis points upwards (hence 'minus value')
                 *
                 * == (   DAHEIGHT * (CHN)        // channel offset
                 *      + (DAHEIGHT / 2) - 0.5    // vertical center -- '0'
                 *      - (DAHEIGHT / 2) * (VAL) * (GAIN)
                 *    )
                 */
                const float chn_y_offset = DAHEIGHT * c + DAHEIGHT * 0.5f - 0.5f;
                const float chn_y_scale  = DAHEIGHT * 0.5f * gain;

#define CYPOS(VAL) (chn_y_offset - (VAL) * chn_y_scale)

                cairo_save(cr);

                /* Restrict drawing to current channel area, don't bleed drawing into
                   neighboring channels. */
                cairo_rectangle(cr, 0, DAHEIGHT * c, DAWIDTH, DAHEIGHT);
                cairo_clip(cr);

                // Set color of wave-form
                cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 1.0);

                /* This is a somewhat 'smart' mechanism to plot audio data using
                   alternating up/down line-directions.  It works well for both cases:
                   1 pixel <= 1 sample and 1 pixel represents more than 1 sample, but
                   is not ideal for either. */
                if (start == chn->idx) {
                        cairo_move_to(cr, start - 0.5, CYPOS(0));
                } else {
                        cairo_move_to(cr, start - 0.5, CYPOS(chn->data_max[start]));
                }

                uint32_t pathlength = 0;
                for (uint32_t i = start; i < end; ++i) {
                        if (i == chn->idx) {
                                continue;
                        } else if (i % 2) {
                                cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i]));
                                cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i]));
                                ++pathlength;
                        } else {
                                cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i]));
                                cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i]));
                                ++pathlength;
                        }

Limit the max cairo path length. This is an optimization trade off: too short path: high load CPU/GPU load. too-long path: bad anti-aliasing, or possibly lost points

                        if (pathlength > MAX_CAIRO_PATH) {
                                pathlength = 0;
                                cairo_stroke(cr);
                                if (i % 2) {
                                        cairo_move_to(cr, i - .5, CYPOS(chn->data_max[i]));
                                } else {
                                        cairo_move_to(cr, i - .5, CYPOS(chn->data_min[i]));
                                }
                        }
                }

                if (pathlength > 0) {
                        cairo_stroke(cr);
                }

                // Draw current position vertical line if display is slow
                if (ui->stride >= ui->rate / 4800.0f || ui->paused) {
                        cairo_set_source_rgba(cr, .9, .2, .2, .6);
                        cairo_move_to(cr, chn->idx - .5, DAHEIGHT * c);
                        cairo_line_to(cr, chn->idx - .5, DAHEIGHT * (c + 1));
                        cairo_stroke(cr);
                }

                // Undo the 'clipping' restriction
                cairo_restore(cr);

                // Channel separator
                if (c > 0) {
                        cairo_set_source_rgba(cr, .5, .5, .5, 1.0);
                        cairo_move_to(cr, 0, DAHEIGHT * c - .5);
                        cairo_line_to(cr, DAWIDTH, DAHEIGHT * c - .5);
                        cairo_stroke(cr);
                }

                // Zero scale line
                cairo_set_source_rgba(cr, .3, .3, .7, .5);
                cairo_move_to(cr, 0, DAHEIGHT * (c + .5) - .5);
                cairo_line_to(cr, DAWIDTH, DAHEIGHT * (c + .5) - .5);
                cairo_stroke(cr);
        }

        cairo_destroy(cr);
        return TRUE;
}

Parse raw audio data and prepare for later drawing.

Note this is a toy example, which is really a waveform display, not an oscilloscope. A serious scope would not display samples as is.

Signals above ~ 1/10 of the sampling-rate will not yield a useful visual display and result in a rather unintuitive representation of the actual waveform.

Ideally the audio-data would be buffered and upsampled here and after that written in a display buffer for later use.

static int
process_channel(EgScopeUI*   ui,
                ScoChan*     chn,
                const size_t n_elem,
                float const* data,
                uint32_t*    idx_start,
                uint32_t*    idx_end)
{
        int overflow = 0;
        *idx_start = chn->idx;
        for (size_t i = 0; i < n_elem; ++i) {
                if (data[i] < chn->data_min[chn->idx]) {
                        chn->data_min[chn->idx] = data[i];
                }
                if (data[i] > chn->data_max[chn->idx]) {
                        chn->data_max[chn->idx] = data[i];
                }
                if (++chn->sub >= ui->stride) {
                        chn->sub = 0;
                        chn->idx = (chn->idx + 1) % DAWIDTH;
                        if (chn->idx == 0) {
                                ++overflow;
                        }
                        chn->data_min[chn->idx] = 1.0;
                        chn->data_max[chn->idx] = -1.0;
                }
        }
        *idx_end = chn->idx;
        return overflow;
}

Called via port_event() which is called by the host, typically at a rate of around 25 FPS.

static void
update_scope(EgScopeUI*    ui,
             const int32_t channel,
             const size_t  n_elem,
             float const*  data)
{
        // Never trust input data which could lead to application failure.
        if (channel < 0 || (uint32_t)channel > ui->n_channels) {
                return;
        }

        // Update state in sync with 1st channel
        if (channel == 0) {
                ui->stride = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_speed));
                const bool paused = gtk_toggle_button_get_active(
                        GTK_TOGGLE_BUTTON(ui->btn_pause));

                if (paused != ui->paused) {
                        ui->paused = paused;
                        gtk_widget_queue_draw(ui->darea);
                }
        }
        if (ui->paused) {
                return;
        }

        uint32_t idx_start;  // Display pixel start
        uint32_t idx_end;    // Display pixel end
        int      overflow;   // Received more audio-data than display-pixel

        // Process this channel's audio-data for display
        ScoChan* chn = &ui->chn[channel];
        overflow = process_channel(ui, chn, n_elem, data, &idx_start, &idx_end);

        // Signal gtk's main thread to redraw the widget after the last channel
        if ((uint32_t)channel + 1 == ui->n_channels) {
                if (overflow > 1) {
                        // Redraw complete widget
                        gtk_widget_queue_draw(ui->darea);
                } else if (idx_end > idx_start) {
                        // Redraw area between start -> end pixel
                        gtk_widget_queue_draw_area(ui->darea, idx_start - 2, 0, 3
                                                   + idx_end - idx_start,
                                                   DAHEIGHT * ui->n_channels);
                } else if (idx_end < idx_start) {
                        // Wrap-around: redraw area between 0->start AND end->right-end
                        gtk_widget_queue_draw_area(
                                ui->darea,
                                idx_start - 2, 0,
                                3 + DAWIDTH - idx_start, DAHEIGHT * ui->n_channels);
                        gtk_widget_queue_draw_area(
                                ui->darea,
                                0, 0,
                                idx_end + 1, DAHEIGHT * ui->n_channels);
                }
        }
}

static LV2UI_Handle
instantiate(const LV2UI_Descriptor*   descriptor,
            const char*               plugin_uri,
            const char*               bundle_path,
            LV2UI_Write_Function      write_function,
            LV2UI_Controller          controller,
            LV2UI_Widget*             widget,
            const LV2_Feature* const* features)
{
        EgScopeUI* ui = (EgScopeUI*)malloc(sizeof(EgScopeUI));

        if (!ui) {
                fprintf(stderr, "EgScope.lv2 UI: out of memory\n");
                return NULL;
        }

        ui->map = NULL;
        *widget = NULL;

        if (!strcmp(plugin_uri, SCO_URI "#Mono")) {
                ui->n_channels = 1;
        } else if (!strcmp(plugin_uri, SCO_URI "#Stereo")) {
                ui->n_channels = 2;
        } else {
                free(ui);
                return NULL;
        }

        for (int i = 0; features[i]; ++i) {
                if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) {
                        ui->map = (LV2_URID_Map*)features[i]->data;
                }
        }

        if (!ui->map) {
                fprintf(stderr, "EgScope.lv2 UI: Host does not support urid:map\n");
                free(ui);
                return NULL;
        }

        // Initialize private data structure
        ui->write      = write_function;
        ui->controller = controller;

        ui->vbox   = NULL;
        ui->hbox   = NULL;
        ui->darea  = NULL;
        ui->stride = 25;
        ui->paused = false;
        ui->rate   = 48000;

        ui->chn[0].idx = 0;
        ui->chn[0].sub = 0;
        ui->chn[1].idx = 0;
        ui->chn[1].sub = 0;
        memset(ui->chn[0].data_min, 0, sizeof(float) * DAWIDTH);
        memset(ui->chn[0].data_max, 0, sizeof(float) * DAWIDTH);
        memset(ui->chn[1].data_min, 0, sizeof(float) * DAWIDTH);
        memset(ui->chn[1].data_max, 0, sizeof(float) * DAWIDTH);

        map_sco_uris(ui->map, &ui->uris);
        lv2_atom_forge_init(&ui->forge, ui->map);

        // Setup UI
        ui->hbox = gtk_hbox_new(FALSE, 0);
        ui->vbox = gtk_vbox_new(FALSE, 0);

        ui->darea = gtk_drawing_area_new();
        gtk_widget_set_size_request(ui->darea, DAWIDTH, DAHEIGHT * ui->n_channels);

        ui->lbl_speed = gtk_label_new("Samples/Pixel");
        ui->lbl_amp   = gtk_label_new("Amplitude");

        ui->sep[0]    = gtk_hseparator_new();
        ui->sep[1]    = gtk_label_new("");
        ui->btn_pause = gtk_toggle_button_new_with_label("Pause");

        ui->spb_speed_adj = (GtkAdjustment*)gtk_adjustment_new(
                        25.0, 1.0, 1000.0, 1.0, 5.0, 0.0);
        ui->spb_speed = gtk_spin_button_new(ui->spb_speed_adj, 1.0, 0);

        ui->spb_amp_adj = (GtkAdjustment*)gtk_adjustment_new(
                1.0, 0.1, 6.0, 0.1, 1.0, 0.0);
        ui->spb_amp = gtk_spin_button_new(ui->spb_amp_adj, 0.1, 1);

        gtk_box_pack_start(GTK_BOX(ui->hbox), ui->darea,     FALSE, FALSE, 0);
        gtk_box_pack_start(GTK_BOX(ui->hbox), ui->vbox,      FALSE, FALSE, 4);

        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_speed, FALSE, FALSE, 2);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_speed, FALSE, FALSE, 2);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[0],    FALSE, FALSE, 8);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_amp,   FALSE, FALSE, 2);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_amp,   FALSE, FALSE, 2);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[1],     TRUE, FALSE, 8);
        gtk_box_pack_start(GTK_BOX(ui->vbox), ui->btn_pause, FALSE, FALSE, 2);

        g_signal_connect(G_OBJECT(ui->darea), "expose_event",
                         G_CALLBACK(on_expose_event), ui);
        g_signal_connect(G_OBJECT(ui->spb_amp), "value-changed",
                         G_CALLBACK(on_cfg_changed), ui);
        g_signal_connect(G_OBJECT(ui->spb_speed), "value-changed",
                         G_CALLBACK(on_cfg_changed), ui);

        *widget = ui->hbox;

        /* Send UIOn message to plugin, which will request state and enable message
           transmission. */
        send_ui_enable(ui);

        return ui;
}

static void
cleanup(LV2UI_Handle handle)
{
        EgScopeUI* ui = (EgScopeUI*)handle;
        /* Send UIOff message to plugin, which will save state and disable message
         * transmission. */
        send_ui_disable(ui);
        gtk_widget_destroy(ui->darea);
        free(ui);
}

static int
recv_raw_audio(EgScopeUI* ui, const LV2_Atom_Object* obj)
{
        const LV2_Atom* chan_val = NULL;
        const LV2_Atom* data_val = NULL;
        const int n_props  = lv2_atom_object_get(
                obj,
                ui->uris.channelID, &chan_val,
                ui->uris.audioData, &data_val,
                NULL);

        if (n_props != 2 ||
            chan_val->type != ui->uris.atom_Int ||
            data_val->type != ui->uris.atom_Vector) {
                // Object does not have the required properties with correct types
                fprintf(stderr, "eg-scope.lv2 UI error: Corrupt audio message\n");
                return 1;
        }

        // Get the values we need from the body of the property value atoms
        const int32_t          chn = ((const LV2_Atom_Int*)chan_val)->body;
        const LV2_Atom_Vector* vec = (const LV2_Atom_Vector*)data_val;
        if (vec->body.child_type != ui->uris.atom_Float) {
                return 1;  // Vector has incorrect element type
        }

        // Number of elements = (total size - header size) / element size
        const size_t n_elem = ((data_val->size - sizeof(LV2_Atom_Vector_Body))
                               / sizeof(float));

        // Float elements immediately follow the vector body header
        const float* data = (const float*)(&vec->body + 1);

        // Update display
        update_scope(ui, chn, n_elem, data);
        return 0;
}

static int
recv_ui_state(EgScopeUI* ui, const LV2_Atom_Object* obj)
{
        const LV2_Atom* spp_val  = NULL;
        const LV2_Atom* amp_val  = NULL;
        const LV2_Atom* rate_val = NULL;
        const int n_props  = lv2_atom_object_get(
                obj,
                ui->uris.ui_spp, &spp_val,
                ui->uris.ui_amp, &amp_val,
                ui->uris.param_sampleRate, &rate_val,
                NULL);

        if (n_props != 3 ||
                spp_val->type != ui->uris.atom_Int ||
                amp_val->type != ui->uris.atom_Float ||
            rate_val->type != ui->uris.atom_Float) {
                // Object does not have the required properties with correct types
                fprintf(stderr, "eg-scope.lv2 UI error: Corrupt state message\n");
                return 1;
        }

        // Get the values we need from the body of the property value atoms
        const int32_t spp  = ((const LV2_Atom_Int*)spp_val)->body;
        const float   amp  = ((const LV2_Atom_Float*)amp_val)->body;
        const float   rate = ((const LV2_Atom_Float*)rate_val)->body;

        // Update UI
        gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_speed), spp);
        gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_amp),   amp);
        ui->rate = rate;

        return 0;
}

Receive data from the DSP-backend.

This is called by the host, typically at a rate of around 25 FPS.

Ideally this happens regularly and with relatively low latency, but there are no hard guarantees about message delivery.

static void
port_event(LV2UI_Handle handle,
           uint32_t     port_index,
           uint32_t     buffer_size,
           uint32_t     format,
           const void*  buffer)
{
        EgScopeUI*      ui   = (EgScopeUI*)handle;
        const LV2_Atom* atom = (const LV2_Atom*)buffer;

        /* Check type of data received
         *  - format == 0: Control port event (float)
         *  - format > 0:  Message (atom)
         */
        if (format == ui->uris.atom_eventTransfer &&
            atom->type == ui->uris.atom_Blank) {
                const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom;
                if (obj->body.otype == ui->uris.RawAudio) {
                        recv_raw_audio(ui, obj);
                } else if (obj->body.otype == ui->uris.ui_State) {
                        recv_ui_state(ui, obj);
                }
        }
}

static const LV2UI_Descriptor descriptor = {
        SCO_URI "#ui",
        instantiate,
        cleanup,
        port_event,
        NULL
};

LV2_SYMBOL_EXPORT
const LV2UI_Descriptor*
lv2ui_descriptor(uint32_t index)
{
        switch (index) {
        case 0:
                return &descriptor;
        default:
                return NULL;
        }
}