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

LV2 plugins are installed in a “bundle”, a directory with a standard structure. Each bundle has a Turtle file named manifest.ttl which lists the contents of the bundle.

Hosts typically read the manifest of every installed bundle to discover plugins on start-up, so it should be as small as possible for performance reasons. Details that are only useful if the host chooses to load the plugin are stored in other files and linked to from manifest.ttl.

URIs

LV2 makes use of URIs as globally-unique identifiers for resources. For example, the ID of the plugin described here is <http://lv2plug.in/plugins/eg-amp>. Note that URIs are only used as identifiers and don’t necessarily imply that something can be accessed at that address on the web (though that may be the case).

Namespace Prefixes

Turtle files contain many URIs, but prefixes can be defined to improve readability. For example, with the lv2: prefix below, lv2:Plugin can be written instead of <http://lv2plug.in/ns/lv2core#Plugin>.

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

Describing a Plugin

Turtle files contain a set of “statements” which describe resources. This file contains 3 statements:

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>

Firstly, <http://lv2plug.in/plugins/eg-amp> is an LV2 plugin:

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

The predicate “a” is a Turtle shorthand for rdf:type.

The binary of that plugin can be found at <amp.ext>:

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

This file is a template; the token @LIB_EXT@ is replaced by the build system with the appropriate extension for the current platform before installation. For example, in the output manifest.ttl, the binary would be listed as <amp.so>. Relative URIs in manifests are relative to the bundle directory, so this refers to a binary with the given name in the same directory as this manifest.

Finally, more information about this plugin can be found in <amp.ttl>:

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

Abbreviation

This file shows these statements individually for instructive purposes, but the subject <http://lv2plug.in/plugins/eg-amp> is repetitive. Turtle allows the semicolon to be used as a delimiter that repeats the previous subject. For example, this manifest would more realistically be written like so:

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

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" ,
                "简单放大器"@zh ,
                "Einfacher Verstärker"@de ,
                "Simple Amplifier"@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" ,
                        "收益"@zh ,
                        "Verstärkung"@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

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/core/lv2.h"

Include standard C headers

#include <math.h>
#include <stdint.h>
#include <stdlib.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*)calloc(1, 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 identifier, 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)
{
  return index == 0 ? &descriptor : 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 "lv2/atom/atom.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/midi/midi.h"
#include "lv2/urid/urid.h"

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
  LV2_Log_Logger logger;

  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)
{
  Midigate* self = (Midigate*)calloc(1, sizeof(Midigate));
  if (!self) {
    return NULL;
  }

  // Scan host features for URID map
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,  &self->logger.log, false,
    LV2_URID__map, &self->map,        true,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->logger, self->map);
  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    free(self);
    return NULL;
  }

  self->uris.midi_MidiEvent =
    self->map->map(self->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:
        if (self->n_active_notes > 0) {
          --self->n_active_notes;
        }
        break;
      case LV2_MIDI_MSG_CONTROLLER:
        if (msg[1] == LV2_MIDI_CTL_ALL_NOTES_OFF) {
          self->n_active_notes = 0;
        }
        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)
{
  return index == 0 ? &descriptor : 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 "./uris.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/midi/midi.h"
#include "lv2/urid/urid.h"

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

enum { FIFTHS_IN = 0, FIFTHS_OUT = 1 };

typedef struct {
  // Features
  LV2_URID_Map*  map;
  LV2_Log_Logger logger;

  // 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*)calloc(1, sizeof(Fifths));
  if (!self) {
    return NULL;
  }

  // Scan host features for URID map
  // clang-format off
  const char*  missing = lv2_features_query(
    features,
    LV2_LOG__log,  &self->logger.log, false,
    LV2_URID__map, &self->map,        true,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->logger, self->map);
  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    free(self);
    return NULL;
  }

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

        if (msg[1] <= 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)
{
  return index == 0 ? &descriptor : NULL;
}

uris.h

#ifndef FIFTHS_URIS_H
#define FIFTHS_URIS_H

#include "lv2/atom/atom.h"
#include "lv2/midi/midi.h"
#include "lv2/patch/patch.h"
#include "lv2/urid/urid.h"

#define EG_FIFTHS_URI "http://lv2plug.in/plugins/eg-fifths"

typedef struct {
  LV2_URID atom_Path;
  LV2_URID atom_Resource;
  LV2_URID atom_Sequence;
  LV2_URID atom_URID;
  LV2_URID atom_eventTransfer;
  LV2_URID midi_Event;
  LV2_URID patch_Set;
  LV2_URID patch_property;
  LV2_URID patch_value;
} FifthsURIs;

static inline void
map_fifths_uris(LV2_URID_Map* map, FifthsURIs* uris)
{
  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->atom_URID          = map->map(map->handle, LV2_ATOM__URID);
  uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
  uris->midi_Event         = map->map(map->handle, LV2_MIDI__MidiEvent);
  uris->patch_Set          = map->map(map->handle, LV2_PATCH__Set);
  uris->patch_property     = map->map(map->handle, LV2_PATCH__property);
  uris->patch_value        = map->map(map->handle, LV2_PATCH__value);
}

#endif /* FIFTHS_URIS_H */

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 "lv2/atom/atom.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/time/time.h"
#include "lv2/urid/urid.h"

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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
  LV2_Log_Logger logger; // Logger API
  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
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,  &self->logger.log, false,
    LV2_URID__map, &self->map, true,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->logger, self->map);
  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    free(self);
    return NULL;
  }

  // Map URIS
  MetroURIs* const    uris  = &self->uris;
  LV2_URID_Map* const map   = self->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 = (uint32_t)(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] * (float)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 - ((float)(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;
  LV2_Atom* bpm   = NULL;
  LV2_Atom* speed = NULL;
  // clang-format off
  lv2_atom_object_get(obj,
                      uris->time_barBeat, &beat,
                      uris->time_beatsPerMinute, &bpm,
                      uris->time_speed, &speed,
                      NULL);
  // clang-format on

  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 = (float)(60.0 / 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           = (uint32_t)(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)
{
  return index == 0 ? &descriptor : 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

  • Network-transparent waveform display with incremental peak transmission

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 param: <http://lv2plug.in/ns/ext/parameters#> .
@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 xsd:   <http://www.w3.org/2001/XMLSchema#> .

<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 "Exampler" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature state:loadDefaultState ,
                urid:map ,
                work:schedule ;
        lv2:optionalFeature lv2:hardRTCapable ,
                state:threadSafeRestore ;
        lv2:extensionData state:interface ,
                work:interface ;
        ui:ui <http://lv2plug.in/plugins/eg-sampler#ui> ;
        patch:writable <http://lv2plug.in/plugins/eg-sampler#sample> ,
                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> ;
                param:gain "0.0"^^xsd:float
        ] .

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

sampler.c

#include "atom_sink.h"
#include "peaks.h"
#include "uris.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/midi/midi.h"
#include "lv2/state/state.h"
#include "lv2/urid/urid.h"
#include "lv2/worker/worker.h"

#include <sndfile.h>

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

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_Logger       logger;

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

  // Communication utilities
  LV2_Atom_Forge_Frame notify_frame; ///< Cached for worker replies
  LV2_Atom_Forge       forge;        ///< Forge for writing atoms in run thread
  PeaksSender          psend;        ///< Audio peaks sender

  // URIs
  SamplerURIs uris;

  // Playback state
  Sample*    sample;
  uint32_t   frame_offset;
  float      gain;
  float      gain_dB;
  sf_count_t frame;
  bool       play;
  bool       activated;
  bool       gain_changed;
  bool       sample_changed;
} 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(LV2_Log_Logger* logger, const char* path)
{
  lv2_log_trace(logger, "Loading %s\n", path);

  const size_t   path_len = strlen(path);
  Sample* const  sample   = (Sample*)calloc(1, sizeof(Sample));
  SF_INFO* const info     = &sample->info;
  SNDFILE* const sndfile  = sf_open(path, SFM_READ, info);
  float*         data     = NULL;
  bool           error    = true;
  if (!sndfile || !info->frames) {
    lv2_log_error(logger, "Failed to open %s\n", path);
  } else if (info->channels != 1) {
    lv2_log_error(logger, "%s has %d channels\n", path, info->channels);
  } else if (!(data = (float*)malloc(sizeof(float) * info->frames))) {
    lv2_log_error(logger, "Failed to allocate memory for sample\n");
  } else {
    error = false;
  }

  if (error) {
    free(sample);
    free(data);
    sf_close(sndfile);
    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 if (atom->type == self->forge.Object) {
    // Handle set message (load sample).
    const LV2_Atom_Object* obj  = (const LV2_Atom_Object*)data;
    const char*            path = read_set_file(&self->uris, obj);
    if (!path) {
      lv2_log_error(&self->logger, "Malformed set file request\n");
      return LV2_WORKER_ERR_UNKNOWN;
    }

    // Load sample.
    Sample* sample = load_sample(&self->logger, path);
    if (sample) {
      // Send new sample 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;
  Sample*  old_sample = self->sample;
  Sample*  new_sample = *(Sample* const*)data;

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

  // Stop playing previous sample, which can be larger than new one
  self->frame = 0;
  self->play  = false;

  // Schedule work to free the old sample
  SampleMessage msg = {{sizeof(Sample*), self->uris.eg_freeSample}, old_sample};
  self->schedule->schedule_work(self->schedule->handle, sizeof(msg), &msg);

  // 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, new_sample->path, new_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*)calloc(1, sizeof(Sampler));
  if (!self) {
    return NULL;
  }

  // Get host features
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,         &self->logger.log, false,
    LV2_URID__map,        &self->map,        true,
    LV2_WORKER__schedule, &self->schedule,   true,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->logger, self->map);
  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    free(self);
    return NULL;
  }

  // Map URIs and initialise forge
  map_sampler_uris(self->map, &self->uris);
  lv2_atom_forge_init(&self->forge, self->map);
  peaks_sender_init(&self->psend, self->map);

  self->gain    = 1.0f;
  self->gain_dB = 0.0f;

  return (LV2_Handle)self;
}

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

static void
activate(LV2_Handle instance)
{
  ((Sampler*)instance)->activated = true;
}

static void
deactivate(LV2_Handle instance)
{
  ((Sampler*)instance)->activated = false;
}

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)

Handle an incoming event in the audio thread.

This performs any actions triggered by an event, such as the start of sample playback, a sample change, or responding to requests from the UI.

static void
handle_event(Sampler* self, LV2_Atom_Event* ev)
{
  SamplerURIs* uris       = &self->uris;
  PeaksURIs*   peaks_uris = &self->psend.uris;

  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:
      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;

      // clang-format off
      lv2_atom_object_get(obj,
                          uris->patch_property, &property,
                          uris->patch_value,    &value,
                          0);
      // clang-format on

      if (!property) {
        lv2_log_error(&self->logger, "Set message with no property\n");
        return;
      }

      if (property->type != uris->atom_URID) {
        lv2_log_error(&self->logger, "Set property is not a URID\n");
        return;
      }

      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, "Scheduling sample change\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 = ((LV2_Atom_Float*)value)->body;
          self->gain    = DB_CO(self->gain_dB);
        }
      }
    } else if (obj->body.otype == uris->patch_Get && self->sample) {
      const LV2_Atom_URID* accept  = NULL;
      const LV2_Atom_Int*  n_peaks = NULL;

      // clang-format off
      lv2_atom_object_get_typed(
        obj,
        uris->patch_accept,      &accept,  uris->atom_URID,
        peaks_uris->peaks_total, &n_peaks, peaks_uris->atom_Int,
        0);
      // clang-format on

      if (accept && accept->body == peaks_uris->peaks_PeakUpdate) {
        // Received a request for peaks, prepare for transmission
        peaks_sender_start(&self->psend,
                           self->sample->data,
                           self->sample->info.frames,
                           n_peaks->body);
      } else {
        // Received a get message, emit our state (probably to UI)
        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 %u\n", obj->body.otype);
    }
  } else {
    lv2_log_trace(&self->logger, "Unknown event type %u\n", ev->body.type);
  }
}

Output audio for a slice of the current cycle.

static void
render(Sampler* self, uint32_t start, uint32_t end)
{
  float* output = self->output_port;

  if (self->play && self->sample) {
    // Start/continue writing sample to output
    for (; start < end; ++start) {
      output[start] = self->sample->data[self->frame] * self->gain;
      if (++self->frame == self->sample->info.frames) {
        self->play = false; // Reached end of sample
        break;
      }
    }
  }

  // Write silence to remaining buffer
  for (; start < end; ++start) {
    output[start] = 0.0f;
  }
}

static void
run(LV2_Handle instance, uint32_t sample_count)
{
  Sampler* self = (Sampler*)instance;

  // 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 gain or sample has changed due to state restore
  if (self->gain_changed || self->sample_changed) {
    lv2_atom_forge_frame_time(&self->forge, 0);

    if (self->gain_changed) {
      write_set_gain(&self->forge, &self->uris, self->gain_dB);
      self->gain_changed = false;
    }

    if (self->sample_changed) {
      write_set_file(
        &self->forge, &self->uris, self->sample->path, self->sample->path_len);
      self->sample_changed = false;
    }
  }

  // Iterate over incoming events, emitting audio along the way
  self->frame_offset = 0;
  LV2_ATOM_SEQUENCE_FOREACH (self->control_port, ev) {
    // Render output up to the time of this event
    render(self, self->frame_offset, ev->time.frames);

    /* Update current frame offset to this event's time.  This is stored in
       the instance because it is used for synchronous worker event
       execution.  This allows a sample load event to be executed with
       sample accuracy when running in a non-realtime context (such as
       exporting a session). */
    self->frame_offset = ev->time.frames;

    // Process this event
    handle_event(self, ev);
  }

  // Use available space after any emitted events to send peaks
  peaks_sender_send(
    &self->psend, &self->forge, sample_count, self->frame_offset);

  // Render output for the rest of the cycle past the last event
  render(self, self->frame_offset, sample_count);
}

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 =
    (LV2_State_Map_Path*)lv2_features_data(features, LV2_STATE__mapPath);
  if (!map_path) {
    return LV2_STATE_ERR_NO_FEATURE;
  }

  // Map absolute sample path to an abstract state path
  char* apath = map_path->abstract_path(map_path->handle, self->sample->path);

  // Store eg:sample = abstract path
  store(handle,
        self->uris.eg_sample,
        apath,
        strlen(apath) + 1,
        self->uris.atom_Path,
        LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);

  free(apath);

  // Store the gain value
  store(handle,
        self->uris.param_gain,
        &self->gain_dB,
        sizeof(self->gain_dB),
        self->uris.atom_Float,
        LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);

  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;

  // Get host features
  LV2_Worker_Schedule* schedule = NULL;
  LV2_State_Map_Path*  paths    = NULL;

  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_STATE__mapPath,   &paths,    true,
    LV2_WORKER__schedule, &schedule, false,
    NULL);
  // clang-format on

  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    return LV2_STATE_ERR_NO_FEATURE;
  }

  // Get eg:sample from state
  size_t      size     = 0;
  uint32_t    type     = 0;
  uint32_t    valflags = 0;
  const void* value =
    retrieve(handle, self->uris.eg_sample, &size, &type, &valflags);

  if (!value) {
    lv2_log_error(&self->logger, "Missing eg:sample\n");
    return LV2_STATE_ERR_NO_PROPERTY;
  }

  if (type != self->uris.atom_Path) {
    lv2_log_error(&self->logger, "Non-path eg:sample\n");
    return LV2_STATE_ERR_BAD_TYPE;
  }

  // Map abstract state path to absolute path
  const char* apath = (const char*)value;
  char*       path  = paths->absolute_path(paths->handle, apath);

  // Replace current sample with the new one
  if (!self->activated || !schedule) {
    // No scheduling available, load sample immediately
    lv2_log_trace(&self->logger, "Synchronous restore\n");
    Sample* sample = load_sample(&self->logger, path);
    if (sample) {
      free_sample(self, self->sample);
      self->sample         = sample;
      self->sample_changed = true;
    }
  } else {
    // Schedule sample to be loaded by the provided worker
    lv2_log_trace(&self->logger, "Scheduling restore\n");
    LV2_Atom_Forge forge;
    LV2_Atom*      buf = (LV2_Atom*)calloc(1, strlen(path) + 128);
    lv2_atom_forge_init(&forge, self->map);
    lv2_atom_forge_set_sink(&forge, atom_sink, atom_sink_deref, buf);
    write_set_file(&forge, &self->uris, path, strlen(path));

    const uint32_t msg_size = lv2_atom_pad_size(buf->size);
    schedule->schedule_work(self->schedule->handle, msg_size, buf + 1);
    free(buf);
  }

  free(path);

  // Get param:gain from state
  value = retrieve(handle, self->uris.param_gain, &size, &type, &valflags);

  if (!value) {
    // Not an error, since older versions did not save this property
    lv2_log_note(&self->logger, "Missing param:gain\n");
    return LV2_STATE_SUCCESS;
  }

  if (type != self->uris.atom_Float) {
    lv2_log_error(&self->logger, "Non-float param:gain\n");
    return LV2_STATE_ERR_BAD_TYPE;
  }

  self->gain_dB      = *(const float*)value;
  self->gain         = DB_CO(self->gain_dB);
  self->gain_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;
  }

  if (!strcmp(uri, LV2_WORKER__interface)) {
    return &worker;
  }

  return NULL;
}

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

LV2_SYMBOL_EXPORT
const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
  return index == 0 ? &descriptor : NULL;
}

sampler_ui.c

#include "peaks.h"
#include "uris.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/midi/midi.h"
#include "lv2/ui/ui.h"
#include "lv2/urid/urid.h"

#include <cairo.h>
#include <gdk/gdk.h>
#include <glib-object.h>
#include <glib.h>
#include <gobject/gclosure.h>
#include <gtk/gtk.h>

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

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

#define MIN_CANVAS_W 128
#define MIN_CANVAS_H 80

typedef struct {
  LV2_Atom_Forge       forge;
  LV2_URID_Map*        map;
  LV2UI_Request_Value* request_value;
  LV2_Log_Logger       logger;
  SamplerURIs          uris;
  PeaksReceiver        precv;

  LV2UI_Write_Function write;
  LV2UI_Controller     controller;

  GtkWidget* box;
  GtkWidget* play_button;
  GtkWidget* file_button;
  GtkWidget* request_file_button;
  GtkWidget* button_box;
  GtkWidget* canvas;

  uint32_t width;
  uint32_t requested_n_peaks;
  char*    filename;

  uint8_t forge_buf[1024];

  // Optional show/hide interface
  GtkWidget* window;
  bool       did_init;
} SamplerUI;

static void
on_file_set(GtkFileChooserButton* widget, void* handle)
{
  SamplerUI* ui = (SamplerUI*)handle;

  // Get the filename from the file chooser
  char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));

  // Write a set message to the plugin to load new file
  lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf));
  LV2_Atom* msg = (LV2_Atom*)write_set_file(
    &ui->forge, &ui->uris, filename, strlen(filename));

  assert(msg);

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

  g_free(filename);
}

static void
on_request_file(GtkButton* widget, void* handle)
{
  SamplerUI* ui = (SamplerUI*)handle;

  ui->request_value->request(
    ui->request_value->handle, ui->uris.eg_sample, 0, NULL);
}

static void
on_play_clicked(GtkFileChooserButton* widget, void* handle)
{
  SamplerUI* ui = (SamplerUI*)handle;
  struct {
    LV2_Atom atom;
    uint8_t  msg[3];
  } note_on;

  note_on.atom.type = ui->uris.midi_Event;
  note_on.atom.size = 3;
  note_on.msg[0]    = LV2_MIDI_MSG_NOTE_ON;
  note_on.msg[1]    = 60;
  note_on.msg[2]    = 60;
  ui->write(ui->controller,
            0,
            sizeof(LV2_Atom) + 3,
            ui->uris.atom_eventTransfer,
            &note_on);
}

static void
request_peaks(SamplerUI* ui, uint32_t n_peaks)
{
  if (n_peaks == ui->requested_n_peaks) {
    return;
  }

  lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf));

  LV2_Atom_Forge_Frame frame;
  lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.patch_Get);
  lv2_atom_forge_key(&ui->forge, ui->uris.patch_accept);
  lv2_atom_forge_urid(&ui->forge, ui->precv.uris.peaks_PeakUpdate);
  lv2_atom_forge_key(&ui->forge, ui->precv.uris.peaks_total);
  lv2_atom_forge_int(&ui->forge, n_peaks);
  lv2_atom_forge_pop(&ui->forge, &frame);

  LV2_Atom* msg = lv2_atom_forge_deref(&ui->forge, frame.ref);
  ui->write(ui->controller,
            0,
            lv2_atom_total_size(msg),
            ui->uris.atom_eventTransfer,
            msg);

  ui->requested_n_peaks = n_peaks;
}

Set Cairo color to a GDK color (to follow Gtk theme).

static void
cairo_set_source_gdk(cairo_t* cr, const GdkColor* color)
{
  cairo_set_source_rgb(
    cr, color->red / 65535.0, color->green / 65535.0, color->blue / 65535.0);
}

static gboolean
on_canvas_expose(GtkWidget* widget, GdkEventExpose* event, gpointer data)
{
  SamplerUI* ui = (SamplerUI*)data;

  GtkAllocation size;
  gtk_widget_get_allocation(widget, &size);

  ui->width = size.width;
  if (ui->width > 2 * ui->requested_n_peaks) {
    request_peaks(ui, 2 * ui->requested_n_peaks);
  }

  cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget));

  cairo_set_line_width(cr, 1.0);
  cairo_translate(cr, 0.5, 0.5);

  const double mid_y = size.height / 2.0;

  const float* const peaks   = ui->precv.peaks;
  const int32_t      n_peaks = ui->precv.n_peaks;
  if (peaks) {
    // Draw waveform
    const double scale = size.width / ((double)n_peaks - 1.0f);

    // Start at left origin
    cairo_move_to(cr, 0, mid_y);

    // Draw line through top peaks
    for (int i = 0; i < n_peaks; ++i) {
      const float peak = peaks[i];
      cairo_line_to(cr, i * scale, mid_y + (peak / 2.0f) * size.height);
    }

    // Continue through bottom peaks
    for (int i = n_peaks - 1; i >= 0; --i) {
      const float peak = peaks[i];
      cairo_line_to(cr, i * scale, mid_y - (peak / 2.0f) * size.height);
    }

    // Close shape
    cairo_line_to(cr, 0, mid_y);

    cairo_set_source_gdk(cr, widget->style->mid);
    cairo_fill_preserve(cr);

    cairo_set_source_gdk(cr, widget->style->fg);
    cairo_stroke(cr);
  }

  cairo_destroy(cr);
  return TRUE;
}

static void
destroy_window(SamplerUI* ui)
{
  if (ui->window) {
    gtk_container_remove(GTK_CONTAINER(ui->window), ui->box);
    gtk_widget_destroy(ui->window);
    ui->window = NULL;
  }
}

static gboolean
on_window_closed(GtkWidget* widget, GdkEvent* event, gpointer data)
{
  SamplerUI* ui = (SamplerUI*)data;

  // Remove widget so Gtk doesn't delete it when the window is closed
  gtk_container_remove(GTK_CONTAINER(ui->window), ui->box);
  ui->window = NULL;

  return FALSE;
}

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*)calloc(1, sizeof(SamplerUI));
  if (!ui) {
    return NULL;
  }

  ui->write      = write_function;
  ui->controller = controller;
  ui->width      = MIN_CANVAS_W;
  *widget        = NULL;
  ui->window     = NULL;
  ui->did_init   = false;

  // Get host features
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,         &ui->logger.log,    false,
    LV2_URID__map,        &ui->map,           true,
    LV2_UI__requestValue, &ui->request_value, false,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&ui->logger, ui->map);
  if (missing) {
    lv2_log_error(&ui->logger, "Missing feature <%s>\n", missing);
    free(ui);
    return NULL;
  }

  // Map URIs and initialise forge
  map_sampler_uris(ui->map, &ui->uris);
  lv2_atom_forge_init(&ui->forge, ui->map);
  peaks_receiver_init(&ui->precv, ui->map);

  // Construct Gtk UI
  ui->box         = gtk_vbox_new(FALSE, 4);
  ui->play_button = gtk_button_new_with_label("▶");
  ui->canvas      = gtk_drawing_area_new();
  ui->button_box  = gtk_hbox_new(FALSE, 4);
  ui->file_button =
    gtk_file_chooser_button_new("Load Sample", GTK_FILE_CHOOSER_ACTION_OPEN);
  ui->request_file_button = gtk_button_new_with_label("Request Sample");
  gtk_widget_set_size_request(ui->canvas, MIN_CANVAS_W, MIN_CANVAS_H);
  gtk_container_set_border_width(GTK_CONTAINER(ui->box), 4);
  gtk_box_pack_start(GTK_BOX(ui->box), ui->canvas, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(ui->box), ui->button_box, FALSE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(ui->button_box), ui->play_button, FALSE, FALSE, 0);
  gtk_box_pack_start(
    GTK_BOX(ui->button_box), ui->request_file_button, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(ui->button_box), ui->file_button, TRUE, TRUE, 0);

  g_signal_connect(ui->file_button, "file-set", G_CALLBACK(on_file_set), ui);

  g_signal_connect(
    ui->request_file_button, "clicked", G_CALLBACK(on_request_file), ui);

  g_signal_connect(ui->play_button, "clicked", G_CALLBACK(on_play_clicked), ui);

  g_signal_connect(
    G_OBJECT(ui->canvas), "expose_event", G_CALLBACK(on_canvas_expose), ui);

  // Request state (filename) from plugin
  lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf));
  LV2_Atom_Forge_Frame frame;
  LV2_Atom*            msg =
    (LV2_Atom*)lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.patch_Get);

  assert(msg);

  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;

  if (ui->window) {
    destroy_window(ui);
  }

  gtk_widget_destroy(ui->canvas);
  gtk_widget_destroy(ui->play_button);
  gtk_widget_destroy(ui->file_button);
  gtk_widget_destroy(ui->request_file_button);
  gtk_widget_destroy(ui->button_box);
  gtk_widget_destroy(ui->box);
  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;
      if (obj->body.otype == ui->uris.patch_Set) {
        const char* path = read_set_file(&ui->uris, obj);
        if (path && (!ui->filename || !!strcmp(path, ui->filename))) {
          g_free(ui->filename);
          ui->filename = g_strdup(path);
          gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(ui->file_button),
                                        path);
          peaks_receiver_clear(&ui->precv);
          ui->requested_n_peaks = 0;
          request_peaks(ui, ui->width / 2 * 2);
        } else if (!path) {
          lv2_log_warning(&ui->logger, "Set message has no path\n");
        }
      } else if (obj->body.otype == ui->precv.uris.peaks_PeakUpdate) {
        if (!peaks_receiver_receive(&ui->precv, obj)) {
          gtk_widget_queue_draw(ui->canvas);
        }
      }
    } else {
      lv2_log_error(&ui->logger, "Unknown message type\n");
    }
  } else {
    lv2_log_warning(&ui->logger, "Unknown port event format\n");
  }
}

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

  if (ui->window) {
    return 0;
  }

  if (!ui->did_init) {
    int argc = 0;
    gtk_init_check(&argc, NULL);
    g_object_ref(ui->box);
    ui->did_init = true;
  }

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

  g_signal_connect(
    G_OBJECT(ui->window), "delete-event", G_CALLBACK(on_window_closed), handle);

  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)
{
  SamplerUI* ui = (SamplerUI*)handle;

  if (ui->window) {
    destroy_window(ui);
  }

  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_do(false);
  }
  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;
  }

  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)
{
  return index == 0 ? &descriptor : NULL;
}

atom_sink.h

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"

#include <stdint.h>
#include <string.h>

A forge sink that writes to an atom buffer.

It is assumed that the handle points to an LV2_Atom large enough to store the forge output. The forged result is in the body of the buffer atom.

static LV2_Atom_Forge_Ref
atom_sink(LV2_Atom_Forge_Sink_Handle handle, const void* buf, uint32_t size)
{
  LV2_Atom*      atom   = (LV2_Atom*)handle;
  const uint32_t offset = lv2_atom_total_size(atom);
  memcpy((char*)atom + offset, buf, size);
  atom->size += size;
  return offset;
}

Dereference counterpart to atom_sink().

static LV2_Atom*
atom_sink_deref(LV2_Atom_Forge_Sink_Handle handle, LV2_Atom_Forge_Ref ref)
{
  return (LV2_Atom*)((char*)handle + ref);
}

peaks.h

#ifndef PEAKS_H_INCLUDED
#define PEAKS_H_INCLUDED

This file defines utilities for sending and receiving audio peaks for waveform display. The functionality is divided into two objects: PeaksSender, for sending peaks updates from the plugin, and PeaksReceiver, for receiving such updates and caching the peaks.

This allows peaks for a waveform of any size at any resolution to be requested, with reasonably sized incremental updates sent over plugin ports.

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/urid/urid.h"

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define PEAKS_URI "http://lv2plug.in/ns/peaks#"
#define PEAKS__PeakUpdate PEAKS_URI "PeakUpdate"
#define PEAKS__magnitudes PEAKS_URI "magnitudes"
#define PEAKS__offset PEAKS_URI "offset"
#define PEAKS__total PEAKS_URI "total"

#ifndef MIN
#  define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
#ifndef MAX
#  define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif

typedef struct {
  LV2_URID atom_Float;
  LV2_URID atom_Int;
  LV2_URID atom_Vector;
  LV2_URID peaks_PeakUpdate;
  LV2_URID peaks_magnitudes;
  LV2_URID peaks_offset;
  LV2_URID peaks_total;
} PeaksURIs;

typedef struct {
  PeaksURIs    uris;           ///< URIDs used in protocol
  const float* samples;        ///< Sample data
  uint32_t     n_samples;      ///< Total number of samples
  uint32_t     n_peaks;        ///< Total number of peaks
  uint32_t     current_offset; ///< Current peak offset
  bool         sending;        ///< True iff currently sending
} PeaksSender;

typedef struct {
  PeaksURIs uris;    ///< URIDs used in protocol
  float*    peaks;   ///< Received peaks, or zeroes
  uint32_t  n_peaks; ///< Total number of peaks
} PeaksReceiver;

Map URIs used in the peaks protocol.

static inline void
peaks_map_uris(PeaksURIs* uris, LV2_URID_Map* map)
{
  uris->atom_Float       = map->map(map->handle, LV2_ATOM__Float);
  uris->atom_Int         = map->map(map->handle, LV2_ATOM__Int);
  uris->atom_Vector      = map->map(map->handle, LV2_ATOM__Vector);
  uris->peaks_PeakUpdate = map->map(map->handle, PEAKS__PeakUpdate);
  uris->peaks_magnitudes = map->map(map->handle, PEAKS__magnitudes);
  uris->peaks_offset     = map->map(map->handle, PEAKS__offset);
  uris->peaks_total      = map->map(map->handle, PEAKS__total);
}

Initialise peaks sender. The new sender is inactive and will do nothing when peaks_sender_send() is called, until a transmission is started with peaks_sender_start().

static inline PeaksSender*
peaks_sender_init(PeaksSender* sender, LV2_URID_Map* map)
{
  memset(sender, 0, sizeof(*sender));
  peaks_map_uris(&sender->uris, map);
  return sender;
}

Prepare to start a new peaks transmission. After this is called, the peaks can be sent with successive calls to peaks_sender_send().

static inline void
peaks_sender_start(PeaksSender* sender,
                   const float* samples,
                   uint32_t     n_samples,
                   uint32_t     n_peaks)
{
  sender->samples        = samples;
  sender->n_samples      = n_samples;
  sender->n_peaks        = n_peaks;
  sender->current_offset = 0;
  sender->sending        = true;
}

Forge a message which sends a range of peaks. Writes a peaks:PeakUpdate object to forge, like:

[]
a peaks:PeakUpdate ;
peaks:offset 256 ;
peaks:total 1024 ;
peaks:magnitudes [ 0.2f, 0.3f, ... ] .
static inline bool
peaks_sender_send(PeaksSender*    sender,
                  LV2_Atom_Forge* forge,
                  uint32_t        n_frames,
                  uint32_t        offset)
{
  const PeaksURIs* uris = &sender->uris;
  if (!sender->sending || sender->current_offset >= sender->n_peaks) {
    return sender->sending = false;
  }

  // Start PeakUpdate object
  lv2_atom_forge_frame_time(forge, offset);
  LV2_Atom_Forge_Frame frame;
  lv2_atom_forge_object(forge, &frame, 0, uris->peaks_PeakUpdate);

  // eg:offset = OFFSET
  lv2_atom_forge_key(forge, uris->peaks_offset);
  lv2_atom_forge_int(forge, sender->current_offset);

  // eg:total = TOTAL
  lv2_atom_forge_key(forge, uris->peaks_total);
  lv2_atom_forge_int(forge, sender->n_peaks);

  // eg:magnitudes = Vector<Float>(PEAK, PEAK, ...)
  lv2_atom_forge_key(forge, uris->peaks_magnitudes);
  LV2_Atom_Forge_Frame vec_frame;
  lv2_atom_forge_vector_head(
    forge, &vec_frame, sizeof(float), uris->atom_Float);

  // Calculate how many peaks to send this update
  const uint32_t chunk_size = MAX(1u, sender->n_samples / sender->n_peaks);
  const uint32_t space      = forge->size - forge->offset;
  const uint32_t remaining  = sender->n_peaks - sender->current_offset;
  const uint32_t n_update =
    MIN(remaining, MIN(n_frames / 4u, space / sizeof(float)));

  // Calculate peak (maximum magnitude) for each chunk
  for (uint32_t i = 0; i < n_update; ++i) {
    const uint32_t start = (sender->current_offset + i) * chunk_size;
    float          peak  = 0.0f;
    for (uint32_t j = 0; j < chunk_size; ++j) {
      peak = fmaxf(peak, fabsf(sender->samples[start + j]));
    }
    lv2_atom_forge_float(forge, peak);
  }

  // Finish message
  lv2_atom_forge_pop(forge, &vec_frame);
  lv2_atom_forge_pop(forge, &frame);

  sender->current_offset += n_update;
  return true;
}

Initialise a peaks receiver. The receiver stores an array of all peaks, which is updated incrementally with peaks_receiver_receive().

static inline PeaksReceiver*
peaks_receiver_init(PeaksReceiver* receiver, LV2_URID_Map* map)
{
  memset(receiver, 0, sizeof(*receiver));
  peaks_map_uris(&receiver->uris, map);
  return receiver;
}

Clear stored peaks and free all memory. This should be called when the peaks are to be updated with a different audio source.

static inline void
peaks_receiver_clear(PeaksReceiver* receiver)
{
  free(receiver->peaks);
  receiver->peaks   = NULL;
  receiver->n_peaks = 0;
}

Handle PeakUpdate message.

The stored peaks array is updated with the slice of peaks in update, resizing if necessary while preserving contents.

Returns 0 if peaks have been updated, negative on error.

static inline int
peaks_receiver_receive(PeaksReceiver* receiver, const LV2_Atom_Object* update)
{
  const PeaksURIs* uris = &receiver->uris;

  // Get properties of interest from update
  const LV2_Atom_Int*    offset = NULL;
  const LV2_Atom_Int*    total  = NULL;
  const LV2_Atom_Vector* peaks  = NULL;

  // clang-format off
  lv2_atom_object_get_typed(update,
                            uris->peaks_offset,     &offset, uris->atom_Int,
                            uris->peaks_total,      &total,  uris->atom_Int,
                            uris->peaks_magnitudes, &peaks,  uris->atom_Vector,
                            0);
  // clang-format on

  if (!offset || !total || !peaks ||
      peaks->body.child_type != uris->atom_Float) {
    return -1; // Invalid update
  }

  const uint32_t n = (uint32_t)total->body;
  if (receiver->n_peaks != n) {
    // Update is for a different total number of peaks, resize
    receiver->peaks = (float*)realloc(receiver->peaks, n * sizeof(float));
    if (receiver->n_peaks > 0 && receiver->n_peaks < n) {
      /* The peaks array is being expanded.  Copy the old peaks,
         duplicating each as necessary to fill the new peaks buffer.
         This preserves the current peaks so that the peaks array can be
         reasonably drawn at any time, but the resolution will increase
         as new updates arrive. */
      const int64_t n_per = n / receiver->n_peaks;
      for (int64_t i = n - 1; i >= 0; --i) {
        receiver->peaks[i] = receiver->peaks[i / n_per];
      }
    } else if (receiver->n_peaks > 0) {
      /* The peak array is being shrunk.  Similar to the above. */
      const int64_t n_per = receiver->n_peaks / n;
      for (int64_t i = n - 1; i >= 0; --i) {
        receiver->peaks[i] = receiver->peaks[i * n_per];
      }
    }
    receiver->n_peaks = n;
  }

  // Copy vector contents to corresponding range in peaks array
  memcpy(receiver->peaks + offset->body,
         peaks + 1,
         peaks->atom.size - sizeof(LV2_Atom_Vector_Body));

  return 0;
}

#endif // PEAKS_H_INCLUDED

uris.h

#ifndef SAMPLER_URIS_H
#define SAMPLER_URIS_H

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/midi/midi.h"
#include "lv2/parameters/parameters.h"
#include "lv2/patch/patch.h"
#include "lv2/urid/urid.h"

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

#define EG_SAMPLER_URI "http://lv2plug.in/plugins/eg-sampler"
#define EG_SAMPLER__applySample EG_SAMPLER_URI "#applySample"
#define EG_SAMPLER__freeSample EG_SAMPLER_URI "#freeSample"
#define EG_SAMPLER__sample EG_SAMPLER_URI "#sample"

typedef struct {
  LV2_URID atom_Float;
  LV2_URID atom_Path;
  LV2_URID atom_Resource;
  LV2_URID atom_Sequence;
  LV2_URID atom_URID;
  LV2_URID atom_eventTransfer;
  LV2_URID eg_applySample;
  LV2_URID eg_freeSample;
  LV2_URID eg_sample;
  LV2_URID midi_Event;
  LV2_URID param_gain;
  LV2_URID patch_Get;
  LV2_URID patch_Set;
  LV2_URID patch_accept;
  LV2_URID patch_property;
  LV2_URID patch_value;
} SamplerURIs;

static inline void
map_sampler_uris(LV2_URID_Map* map, SamplerURIs* uris)
{
  uris->atom_Float         = map->map(map->handle, LV2_ATOM__Float);
  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->atom_URID          = map->map(map->handle, LV2_ATOM__URID);
  uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
  uris->eg_applySample     = map->map(map->handle, EG_SAMPLER__applySample);
  uris->eg_freeSample      = map->map(map->handle, EG_SAMPLER__freeSample);
  uris->eg_sample          = map->map(map->handle, EG_SAMPLER__sample);
  uris->midi_Event         = map->map(map->handle, LV2_MIDI__MidiEvent);
  uris->param_gain         = map->map(map->handle, LV2_PARAMETERS__gain);
  uris->patch_Get          = map->map(map->handle, LV2_PATCH__Get);
  uris->patch_Set          = map->map(map->handle, LV2_PATCH__Set);
  uris->patch_accept       = map->map(map->handle, LV2_PATCH__accept);
  uris->patch_property     = map->map(map->handle, LV2_PATCH__property);
  uris->patch_value        = map->map(map->handle, LV2_PATCH__value);
}

Write a message like the following to forge:

[]
a patch:Set ;
patch:property param:gain ;
patch:value 0.0f .
static inline LV2_Atom_Forge_Ref
write_set_gain(LV2_Atom_Forge* forge, const SamplerURIs* uris, const float gain)
{
  LV2_Atom_Forge_Frame frame;
  LV2_Atom_Forge_Ref   set =
    lv2_atom_forge_object(forge, &frame, 0, uris->patch_Set);

  lv2_atom_forge_key(forge, uris->patch_property);
  lv2_atom_forge_urid(forge, uris->param_gain);
  lv2_atom_forge_key(forge, uris->patch_value);
  lv2_atom_forge_float(forge, gain);

  lv2_atom_forge_pop(forge, &frame);
  return set;
}

Write a message like the following to forge:

[]
a patch:Set ;
patch:property eg:sample ;
patch:value </home/me/foo.wav> .
static inline LV2_Atom_Forge_Ref
write_set_file(LV2_Atom_Forge*    forge,
               const SamplerURIs* uris,
               const char*        filename,
               const uint32_t     filename_len)
{
  LV2_Atom_Forge_Frame frame;
  LV2_Atom_Forge_Ref   set =
    lv2_atom_forge_object(forge, &frame, 0, uris->patch_Set);

  lv2_atom_forge_key(forge, uris->patch_property);
  lv2_atom_forge_urid(forge, uris->eg_sample);
  lv2_atom_forge_key(forge, uris->patch_value);
  lv2_atom_forge_path(forge, filename, filename_len);

  lv2_atom_forge_pop(forge, &frame);
  return set;
}

Get the file path from obj which is a message like:

[]
a patch:Set ;
patch:property eg:sample ;
patch:value </home/me/foo.wav> .
static inline const char*
read_set_file(const SamplerURIs* uris, const LV2_Atom_Object* obj)
{
  if (obj->body.otype != uris->patch_Set) {
    fprintf(stderr, "Ignoring unknown message type %u\n", obj->body.otype);
    return NULL;
  }

  /* Get property URI. */
  const LV2_Atom* property = NULL;
  lv2_atom_object_get(obj, uris->patch_property, &property, 0);
  if (!property) {
    fprintf(stderr, "Malformed set message has no body.\n");
    return NULL;
  }

  if (property->type != uris->atom_URID) {
    fprintf(stderr, "Malformed set message has non-URID property.\n");
    return NULL;
  }

  if (((const LV2_Atom_URID*)property)->body != uris->eg_sample) {
    fprintf(stderr, "Set message for unknown property.\n");
    return NULL;
  }

  /* Get value. */
  const LV2_Atom* value = NULL;
  lv2_atom_object_get(obj, uris->patch_value, &value, 0);
  if (!value) {
    fprintf(stderr, "Malformed set message has no value.\n");
    return NULL;
  }

  if (value->type != uris->atom_Path) {
    fprintf(stderr, "Set message value is not a Path.\n");
    return NULL;
  }

  return (const char*)LV2_ATOM_BODY_CONST(value);
}

#endif /* SAMPLER_URIS_H */

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 "./uris.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/state/state.h"
#include "lv2/urid/urid.h"

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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_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
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,  &self->logger.log, false,
    LV2_URID__map, &self->map,        true,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->logger, self->map);
  if (missing) {
    lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
    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.0f;

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

  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, (int32_t)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, (float)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 (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 == 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, (int32_t)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     = 0;
  uint32_t type     = 0;
  uint32_t valflags = 0;

  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 "./uris.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/ui/ui.h"
#include "lv2/urid/urid.h"

#include <cairo.h>
#include <gdk/gdk.h>
#include <glib-object.h>
#include <glib.h>
#include <gobject/gclosure.h>
#include <gtk/gtk.h>

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
  bool     updating;
} 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);

  assert(msg);

  // 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);

  assert(msg);

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

  assert(msg);

  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)
{
  EgScopeUI* ui = (EgScopeUI*)data;
  if (!ui->updating) {
    // Only send UI state if the change is from user interaction
    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 = 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;
      }

      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.0f;
      chn->data_max[chn->idx] = -1.0f;
    }
  }
  *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 = 0; // Display pixel start
  uint32_t idx_end   = 0; // Display pixel end
  int      overflow  = 0; // 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*)calloc(1, 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;

  // Disable transmission and update UI
  ui->updating = true;
  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->updating = false;
  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 &&
      lv2_atom_forge_is_object_type(&ui->forge, atom->type)) {
    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)
{
  return index == 0 ? &descriptor : NULL;
}

uris.h

#ifndef SCO_URIS_H
#define SCO_URIS_H

#include "lv2/atom/atom.h"
#include "lv2/parameters/parameters.h"
#include "lv2/urid/urid.h"

#define SCO_URI "http://lv2plug.in/plugins/eg-scope"

typedef struct {
  // URIs defined in LV2 specifications
  LV2_URID atom_Vector;
  LV2_URID atom_Float;
  LV2_URID atom_Int;
  LV2_URID atom_eventTransfer;
  LV2_URID param_sampleRate;

  /* URIs defined for this plugin.  It is best to re-use existing URIs as
     much as possible, but plugins may need more vocabulary specific to their
     needs.  These are used as types and properties for plugin:UI
     communication, as well as for saving state. */
  LV2_URID RawAudio;
  LV2_URID channelID;
  LV2_URID audioData;
  LV2_URID ui_On;
  LV2_URID ui_Off;
  LV2_URID ui_State;
  LV2_URID ui_spp;
  LV2_URID ui_amp;
} ScoLV2URIs;

static inline void
map_sco_uris(LV2_URID_Map* map, ScoLV2URIs* uris)
{
  uris->atom_Vector        = map->map(map->handle, LV2_ATOM__Vector);
  uris->atom_Float         = map->map(map->handle, LV2_ATOM__Float);
  uris->atom_Int           = map->map(map->handle, LV2_ATOM__Int);
  uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
  uris->param_sampleRate   = map->map(map->handle, LV2_PARAMETERS__sampleRate);

  /* Note the convention that URIs for types are capitalized, and URIs for
     everything else (mainly properties) are not, just as in LV2
     specifications. */
  uris->RawAudio  = map->map(map->handle, SCO_URI "#RawAudio");
  uris->audioData = map->map(map->handle, SCO_URI "#audioData");
  uris->channelID = map->map(map->handle, SCO_URI "#channelID");
  uris->ui_On     = map->map(map->handle, SCO_URI "#UIOn");
  uris->ui_Off    = map->map(map->handle, SCO_URI "#UIOff");
  uris->ui_State  = map->map(map->handle, SCO_URI "#UIState");
  uris->ui_spp    = map->map(map->handle, SCO_URI "#ui-spp");
  uris->ui_amp    = map->map(map->handle, SCO_URI "#ui-amp");
}

#endif /* SCO_URIS_H */

Params

The basic LV2 mechanism for controls is lv2:ControlPort, inherited from LADSPA. Control ports are problematic because they are not sample accurate, support only one type (float), and require that plugins poll to know when a control has changed.

Parameters can be used instead to address these issues. Parameters can be thought of as properties of a plugin instance; they are identified by URI and have a value of any type. This deliberately meshes with the concept of plugin state defined by the LV2 state extension. The state extension allows plugins to save and restore their parameters (along with other internal state information, if necessary).

Parameters are accessed and manipulated using messages sent via a sequence port. The LV2 patch extension defines the standard messages for working with parameters. Typically, only two are used for simple plugins: patch:Set sets a parameter to some value, and patch:Get requests that the plugin send a description of its parameters.

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-params>
        a lv2:Plugin ;
        lv2:binary <params@LIB_EXT@> ;
        rdfs:seeAlso <params.ttl> .

params.ttl

@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix plug: <http://lv2plug.in/plugins/eg-params#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

An existing parameter or RDF property can be used as a parameter. The LV2 parameters extension http://lv2plug.in/ns/ext/parameters defines many common audio parameters. Where possible, existing parameters should be used so hosts can intelligently control plugins.

If no suitable parameter exists, one can be defined for the plugin like so:

plug:int
        a lv2:Parameter ;
        rdfs:label "int" ;
        rdfs:range atom:Int .

plug:long
        a lv2:Parameter ;
        rdfs:label "long" ;
        rdfs:range atom:Long .

plug:float
        a lv2:Parameter ;
        rdfs:label "float" ;
        rdfs:range atom:Float .

plug:double
        a lv2:Parameter ;
        rdfs:label "double" ;
        rdfs:range atom:Double .

plug:bool
        a lv2:Parameter ;
        rdfs:label "bool" ;
        rdfs:range atom:Bool .

plug:string
        a lv2:Parameter ;
        rdfs:label "string" ;
        rdfs:range atom:String .

plug:path
        a lv2:Parameter ;
        rdfs:label "path" ;
        rdfs:range atom:Path .

plug:lfo
        a lv2:Parameter ;
        rdfs:label "LFO" ;
        rdfs:range atom:Float ;
        lv2:minimum -1.0 ;
        lv2:maximum 1.0 .

plug:spring
        a lv2:Parameter ;
        rdfs:label "spring" ;
        rdfs:range atom:Float .

Most of the plugin description is similar to the others we have seen, but this plugin has only two ports, for receiving and sending messages used to manipulate and access parameters.

<http://lv2plug.in/plugins/eg-params>
        a lv2:Plugin ,
                lv2:UtilityPlugin ;
        doap:name "Example Parameters" ;
        doap:license <http://opensource.org/licenses/isc> ;
        lv2:project <http://lv2plug.in/ns/lv2> ;
        lv2:requiredFeature urid:map ;
        lv2:optionalFeature lv2:hardRTCapable ,
                state:loadDefaultState ;
        lv2:extensionData state:interface ;
        lv2:port [
                a lv2:InputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports patch:Message ;
                lv2:designation lv2:control ;
                lv2:index 0 ;
                lv2:symbol "in" ;
                lv2:name "In"
        ] , [
                a lv2:OutputPort ,
                        atom:AtomPort ;
                atom:bufferType atom:Sequence ;
                atom:supports patch:Message ;
                lv2:designation lv2:control ;
                lv2:index 1 ;
                lv2:symbol "out" ;
                lv2:name "Out"
        ] ;

The plugin must list all parameters that can be written (e.g. changed by the user) as patch:writable:

        patch:writable plug:int ,
                plug:long ,
                plug:float ,
                plug:double ,
                plug:bool ,
                plug:string ,
                plug:path ,
                plug:spring ;

Similarly, parameters that may change internally must be listed as patch:readable, meaning to host should watch for changes to the parameter’s value:

                patch:readable plug:lfo ,
                        plug:spring ;

Parameters map directly to properties of the plugin’s state. So, we can specify initial parameter values with the state:state property. The state:loadDefaultState feature (required above) requires that the host loads the default state after instantiation but before running the plugin.

        state:state [
                plug:int 0 ;
                plug:long "0"^^xsd:long ;
                plug:float "0.1234"^^xsd:float ;
                plug:double "0e0"^^xsd:double ;
                plug:bool false ;
                plug:string "Hello, world" ;
                plug:path <params.ttl> ;
                plug:spring "0.0"^^xsd:float ;
                plug:lfo "0.0"^^xsd:float
        ] .

params.c

#include "state_map.h"

#include "lv2/atom/atom.h"
#include "lv2/atom/forge.h"
#include "lv2/atom/util.h"
#include "lv2/core/lv2.h"
#include "lv2/core/lv2_util.h"
#include "lv2/log/log.h"
#include "lv2/log/logger.h"
#include "lv2/midi/midi.h"
#include "lv2/patch/patch.h"
#include "lv2/state/state.h"
#include "lv2/urid/urid.h"

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

#define MAX_STRING 1024

#define EG_PARAMS_URI "http://lv2plug.in/plugins/eg-params"

#define N_PROPS 9

typedef struct {
  LV2_URID plugin;
  LV2_URID atom_Path;
  LV2_URID atom_Sequence;
  LV2_URID atom_URID;
  LV2_URID atom_eventTransfer;
  LV2_URID eg_spring;
  LV2_URID midi_Event;
  LV2_URID patch_Get;
  LV2_URID patch_Set;
  LV2_URID patch_Put;
  LV2_URID patch_body;
  LV2_URID patch_subject;
  LV2_URID patch_property;
  LV2_URID patch_value;
} URIs;

typedef struct {
  LV2_Atom_Int    aint;
  LV2_Atom_Long   along;
  LV2_Atom_Float  afloat;
  LV2_Atom_Double adouble;
  LV2_Atom_Bool   abool;
  LV2_Atom        astring;
  char            string[MAX_STRING];
  LV2_Atom        apath;
  char            path[MAX_STRING];
  LV2_Atom_Float  lfo;
  LV2_Atom_Float  spring;
} State;

static inline void
map_uris(LV2_URID_Map* map, URIs* uris)
{
  uris->plugin = map->map(map->handle, EG_PARAMS_URI);

  uris->atom_Path          = map->map(map->handle, LV2_ATOM__Path);
  uris->atom_Sequence      = map->map(map->handle, LV2_ATOM__Sequence);
  uris->atom_URID          = map->map(map->handle, LV2_ATOM__URID);
  uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
  uris->eg_spring          = map->map(map->handle, EG_PARAMS_URI "#spring");
  uris->midi_Event         = map->map(map->handle, LV2_MIDI__MidiEvent);
  uris->patch_Get          = map->map(map->handle, LV2_PATCH__Get);
  uris->patch_Set          = map->map(map->handle, LV2_PATCH__Set);
  uris->patch_Put          = map->map(map->handle, LV2_PATCH__Put);
  uris->patch_body         = map->map(map->handle, LV2_PATCH__body);
  uris->patch_subject      = map->map(map->handle, LV2_PATCH__subject);
  uris->patch_property     = map->map(map->handle, LV2_PATCH__property);
  uris->patch_value        = map->map(map->handle, LV2_PATCH__value);
}

enum { PARAMS_IN = 0, PARAMS_OUT = 1 };

typedef struct {
  // Features
  LV2_URID_Map*   map;
  LV2_URID_Unmap* unmap;
  LV2_Log_Logger  log;

  // Forge for creating atoms
  LV2_Atom_Forge forge;

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

  // URIs
  URIs uris;

  // Plugin state
  StateMapItem props[N_PROPS];
  State        state;

  // Buffer for making strings from URIDs if unmap is not provided
  char urid_buf[12];
} Params;

static void
connect_port(LV2_Handle instance, uint32_t port, void* data)
{
  Params* self = (Params*)instance;
  switch (port) {
  case PARAMS_IN:
    self->in_port = (const LV2_Atom_Sequence*)data;
    break;
  case PARAMS_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 instance
  Params* self = (Params*)calloc(1, sizeof(Params));
  if (!self) {
    return NULL;
  }

  // Get host features
  // clang-format off
  const char* missing = lv2_features_query(
    features,
    LV2_LOG__log,    &self->log.log, false,
    LV2_URID__map,   &self->map,     true,
    LV2_URID__unmap, &self->unmap,   false,
    NULL);
  // clang-format on

  lv2_log_logger_set_map(&self->log, self->map);
  if (missing) {
    lv2_log_error(&self->log, "Missing feature <%s>\n", missing);
    free(self);
    return NULL;
  }

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

  // Initialise state dictionary
  // clang-format off
  State* state = &self->state;
  state_map_init(
    self->props, self->map, self->map->handle,
    EG_PARAMS_URI "#int",    STATE_MAP_INIT(Int,    &state->aint),
    EG_PARAMS_URI "#long",   STATE_MAP_INIT(Long,   &state->along),
    EG_PARAMS_URI "#float",  STATE_MAP_INIT(Float,  &state->afloat),
    EG_PARAMS_URI "#double", STATE_MAP_INIT(Double, &state->adouble),
    EG_PARAMS_URI "#bool",   STATE_MAP_INIT(Bool,   &state->abool),
    EG_PARAMS_URI "#string", STATE_MAP_INIT(String, &state->astring),
    EG_PARAMS_URI "#path",   STATE_MAP_INIT(Path,   &state->apath),
    EG_PARAMS_URI "#lfo",    STATE_MAP_INIT(Float,  &state->lfo),
    EG_PARAMS_URI "#spring", STATE_MAP_INIT(Float,  &state->spring),
    NULL);
  // clang-format on

  return (LV2_Handle)self;
}

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

Helper function to unmap a URID if possible.

static const char*
unmap(Params* self, LV2_URID urid)
{
  if (self->unmap) {
    return self->unmap->unmap(self->unmap->handle, urid);
  }

  snprintf(self->urid_buf, sizeof(self->urid_buf), "%u", urid);
  return self->urid_buf;
}

static LV2_State_Status
check_type(Params* self, LV2_URID key, LV2_URID type, LV2_URID required_type)
{
  if (type != required_type) {
    lv2_log_trace(&self->log,
                  "Bad type <%s> for <%s> (needs <%s>)\n",
                  unmap(self, type),
                  unmap(self, key),
                  unmap(self, required_type));
    return LV2_STATE_ERR_BAD_TYPE;
  }
  return LV2_STATE_SUCCESS;
}

static LV2_State_Status
set_parameter(Params*     self,
              LV2_URID    key,
              uint32_t    size,
              LV2_URID    type,
              const void* body,
              bool        from_state)
{
  // Look up property in state dictionary
  const StateMapItem* entry = state_map_find(self->props, N_PROPS, key);
  if (!entry) {
    lv2_log_trace(&self->log, "Unknown parameter <%s>\n", unmap(self, key));
    return LV2_STATE_ERR_NO_PROPERTY;
  }

  // Ensure given type matches property's type
  if (check_type(self, key, type, entry->value->type)) {
    return LV2_STATE_ERR_BAD_TYPE;
  }

  // Set property value in state dictionary
  lv2_log_trace(&self->log, "Set <%s>\n", entry->uri);
  memcpy(entry->value + 1, body, size);
  entry->value->size = size;
  return LV2_STATE_SUCCESS;
}

static const LV2_Atom*
get_parameter(Params* self, LV2_URID key)
{
  const StateMapItem* entry = state_map_find(self->props, N_PROPS, key);
  if (entry) {
    lv2_log_trace(&self->log, "Get <%s>\n", entry->uri);
    return entry->value;
  }

  lv2_log_trace(&self->log, "Unknown parameter <%s>\n", unmap(self, key));
  return NULL;
}

static LV2_State_Status
write_param_to_forge(LV2_State_Handle handle,
                     uint32_t         key,
                     const void*      value,
                     size_t           size,
                     uint32_t         type,
                     uint32_t         flags)
{
  LV2_Atom_Forge* forge = (LV2_Atom_Forge*)handle;

  if (!lv2_atom_forge_key(forge, key) ||
      !lv2_atom_forge_atom(forge, size, type) ||
      !lv2_atom_forge_write(forge, value, size)) {
    return LV2_STATE_ERR_UNKNOWN;
  }

  return LV2_STATE_SUCCESS;
}

static void
store_prop(Params*                  self,
           LV2_State_Map_Path*      map_path,
           LV2_State_Status*        save_status,
           LV2_State_Store_Function store,
           LV2_State_Handle         handle,
           LV2_URID                 key,
           const LV2_Atom*          value)
{
  LV2_State_Status st = LV2_STATE_SUCCESS;
  if (map_path && value->type == self->uris.atom_Path) {
    // Map path to abstract path for portable storage
    const char* path  = (const char*)(value + 1);
    char*       apath = map_path->abstract_path(map_path->handle, path);
    st                = store(handle,
               key,
               apath,
               strlen(apath) + 1,
               self->uris.atom_Path,
               LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);
    free(apath);
  } else {
    // Store simple property
    st = store(handle,
               key,
               value + 1,
               value->size,
               value->type,
               LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);
  }

  if (save_status && !*save_status) {
    *save_status = st;
  }
}

State save method.

This is used in the usual way when called by the host to save plugin state, but also internally for writing messages in the audio thread by passing a "store" function which actually writes the description to the forge.

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)
{
  Params*             self = (Params*)instance;
  LV2_State_Map_Path* map_path =
    (LV2_State_Map_Path*)lv2_features_data(features, LV2_STATE__mapPath);

  LV2_State_Status st = LV2_STATE_SUCCESS;
  for (unsigned i = 0; i < N_PROPS; ++i) {
    StateMapItem* prop = &self->props[i];
    store_prop(self, map_path, &st, store, handle, prop->urid, prop->value);
  }

  return st;
}

static void
retrieve_prop(Params*                     self,
              LV2_State_Status*           restore_status,
              LV2_State_Retrieve_Function retrieve,
              LV2_State_Handle            handle,
              LV2_URID                    key)
{
  // Retrieve value from saved state
  size_t      vsize  = 0;
  uint32_t    vtype  = 0;
  uint32_t    vflags = 0;
  const void* value  = retrieve(handle, key, &vsize, &vtype, &vflags);

  // Set plugin instance state
  const LV2_State_Status st =
    value ? set_parameter(self, key, vsize, vtype, value, true)
          : LV2_STATE_ERR_NO_PROPERTY;

  if (!*restore_status) {
    *restore_status = st; // Set status if there has been no error yet
  }
}

State restore method.

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)
{
  Params*          self = (Params*)instance;
  LV2_State_Status st   = LV2_STATE_SUCCESS;

  for (unsigned i = 0; i < N_PROPS; ++i) {
    retrieve_prop(self, &st, retrieve, handle, self->props[i].urid);
  }

  return st;
}

static inline bool
subject_is_plugin(Params* self, const LV2_Atom_URID* subject)
{
  // This simple plugin only supports one subject: itself
  return (!subject || (subject->atom.type == self->uris.atom_URID &&
                       subject->body == self->uris.plugin));
}

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

  // Initially, self->out_port contains a Chunk with size set to capacity
  // Set up forge to write directly to output port
  const uint32_t out_capacity = self->out_port->atom.size;
  lv2_atom_forge_set_buffer(
    &self->forge, (uint8_t*)self->out_port, out_capacity);

  // Start a sequence in the output port
  LV2_Atom_Forge_Frame out_frame;
  lv2_atom_forge_sequence_head(&self->forge, &out_frame, 0);

  // Read incoming events
  LV2_ATOM_SEQUENCE_FOREACH (self->in_port, ev) {
    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_URID* subject  = NULL;
      const LV2_Atom_URID* property = NULL;
      const LV2_Atom*      value    = NULL;

      // clang-format off
      lv2_atom_object_get(obj,
                          uris->patch_subject,  (const LV2_Atom**)&subject,
                          uris->patch_property, (const LV2_Atom**)&property,
                          uris->patch_value,    &value,
                          0);
      // clang-format on

      if (!subject_is_plugin(self, subject)) {
        lv2_log_error(&self->log, "Set for unknown subject\n");
      } else if (!property) {
        lv2_log_error(&self->log, "Set with no property\n");
      } else if (property->atom.type != uris->atom_URID) {
        lv2_log_error(&self->log, "Set property is not a URID\n");
      } else {
        // Set property to the given value
        const LV2_URID key = property->body;
        set_parameter(self, key, value->size, value->type, value + 1, false);
      }
    } else if (obj->body.otype == uris->patch_Get) {
      // Get the property of the get message
      const LV2_Atom_URID* subject  = NULL;
      const LV2_Atom_URID* property = NULL;

      // clang-format off
      lv2_atom_object_get(obj,
                          uris->patch_subject,  (const LV2_Atom**)&subject,
                          uris->patch_property, (const LV2_Atom**)&property,
                          0);
      // clang-format on

      if (!subject_is_plugin(self, subject)) {
        lv2_log_error(&self->log, "Get with unknown subject\n");
      } else if (!property) {
        // Get with no property, emit complete state
        lv2_atom_forge_frame_time(&self->forge, ev->time.frames);
        LV2_Atom_Forge_Frame pframe;
        lv2_atom_forge_object(&self->forge, &pframe, 0, uris->patch_Put);
        lv2_atom_forge_key(&self->forge, uris->patch_body);

        LV2_Atom_Forge_Frame bframe;
        lv2_atom_forge_object(&self->forge, &bframe, 0, 0);
        save(self, write_param_to_forge, &self->forge, 0, NULL);

        lv2_atom_forge_pop(&self->forge, &bframe);
        lv2_atom_forge_pop(&self->forge, &pframe);
      } else if (property->atom.type != uris->atom_URID) {
        lv2_log_error(&self->log, "Get property is not a URID\n");
      } else {
        // Get for a specific property
        const LV2_URID  key   = property->body;
        const LV2_Atom* value = get_parameter(self, key);
        if (value) {
          lv2_atom_forge_frame_time(&self->forge, ev->time.frames);
          LV2_Atom_Forge_Frame frame;
          lv2_atom_forge_object(&self->forge, &frame, 0, uris->patch_Set);
          lv2_atom_forge_key(&self->forge, uris->patch_property);
          lv2_atom_forge_urid(&self->forge, property->body);
          store_prop(self,
                     NULL,
                     NULL,
                     write_param_to_forge,
                     &self->forge,
                     uris->patch_value,
                     value);
          lv2_atom_forge_pop(&self->forge, &frame);
        }
      }
    } else {
      lv2_log_trace(
        &self->log, "Unknown object type <%s>\n", unmap(self, obj->body.otype));
    }
  }

  if (self->state.spring.body > 0.0f) {
    const float spring      = self->state.spring.body;
    self->state.spring.body = (spring >= 0.001) ? spring - 0.001f : 0.0f;
    lv2_atom_forge_frame_time(&self->forge, 0);
    LV2_Atom_Forge_Frame frame;
    lv2_atom_forge_object(&self->forge, &frame, 0, uris->patch_Set);

    lv2_atom_forge_key(&self->forge, uris->patch_property);
    lv2_atom_forge_urid(&self->forge, uris->eg_spring);
    lv2_atom_forge_key(&self->forge, uris->patch_value);
    lv2_atom_forge_float(&self->forge, self->state.spring.body);

    lv2_atom_forge_pop(&self->forge, &frame);
  }

  lv2_atom_forge_pop(&self->forge, &out_frame);
}

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

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

LV2_SYMBOL_EXPORT const LV2_Descriptor*
lv2_descriptor(uint32_t index)
{
  return (index == 0) ? &descriptor : NULL;
}

state_map.h

#include "lv2/atom/atom.h"
#include "lv2/urid/urid.h"

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

Entry in an array that serves as a dictionary of properties.

typedef struct {
  const char* uri;
  LV2_URID    urid;
  LV2_Atom*   value;
} StateMapItem;

Comparator for StateMapItems sorted by URID.

static int
state_map_cmp(const void* a, const void* b)
{
  const StateMapItem* ka = (const StateMapItem*)a;
  const StateMapItem* kb = (const StateMapItem*)b;
  if (ka->urid < kb->urid) {
    return -1;
  }

  if (kb->urid < ka->urid) {
    return 1;
  }

  return 0;
}

Helper macro for terse state map initialisation.

#define STATE_MAP_INIT(type, ptr) \
  (LV2_ATOM__##type), (sizeof(*(ptr)) - sizeof(LV2_Atom)), (ptr)

Initialise a state map.

The variable parameters list must be NULL terminated, and is a sequence of const char* uri, const char* type, uint32_t size, LV2_Atom* value. The value must point to a valid atom that resides elsewhere, the state map is only an index and does not contain actual state values. The macro STATE_MAP_INIT can be used to make simpler code when state is composed of standard atom types, for example:

struct Plugin { LV2_URID_Map* map; StateMapItem props[3]; };

state_map_init( self→props, self→map, self→map→handle, PLUG_URI "#gain", STATE_MAP_INIT(Float, &state→gain), PLUG_URI "#offset", STATE_MAP_INIT(Int, &state→offset), PLUG_URI "#file", STATE_MAP_INIT(Path, &state→file), NULL);

static void
state_map_init(
  StateMapItem        dict[],
  LV2_URID_Map*       map,
  LV2_URID_Map_Handle handle,
  /* const char* uri, const char* type, uint32_t size, LV2_Atom* value */...)
{
  // Set dict entries from parameters
  unsigned i = 0;
  va_list  args;
  va_start(args, handle);
  for (const char* uri = NULL; (uri = va_arg(args, const char*)); ++i) {
    const char*     type  = va_arg(args, const char*);
    const uint32_t  size  = va_arg(args, uint32_t);
    LV2_Atom* const value = va_arg(args, LV2_Atom*);
    dict[i].uri           = uri;
    dict[i].urid          = map->map(map->handle, uri);
    dict[i].value         = value;
    dict[i].value->size   = size;
    dict[i].value->type   = map->map(map->handle, type);
  }
  va_end(args);

  // Sort for fast lookup by URID by state_map_find()
  qsort(dict, i, sizeof(StateMapItem), state_map_cmp);
}

Retrieve an item from a state map by URID.

This takes O(lg(n)) time, and is useful for implementing generic property access with little code, for example to respond to patch:Get messages for a specific property.

static StateMapItem*
state_map_find(StateMapItem dict[], uint32_t n_entries, LV2_URID urid)
{
  const StateMapItem key = {NULL, urid, NULL};
  return (StateMapItem*)bsearch(
    &key, dict, n_entries, sizeof(StateMapItem), state_map_cmp);
}