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>
.
Describing a Plugin
Turtle files contain a set of “statements” which describe resources. This file contains 3 statements:
Subject | Predicate | Object |
---|---|---|
a |
lv2:Plugin |
|
lv2:binary |
<amp.so> |
|
rdfs:seeAlso |
<amp.ttl> |
Firstly, <http://lv2plug.in/plugins/eg-amp>
is an LV2 plugin:
The predicate “a
” is a Turtle shorthand for rdf:type
.
The binary of that plugin can be found at <amp.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>
:
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:
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.
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.
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
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.
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.
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.
Ports can describe units and control detents to allow better UI generation and host automation.
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 standard C headers
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.
In code, ports are referred to by index. An enumeration of port indices should be defined for readability.
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.
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.
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().
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.
Define a macro for converting a gain in dB to a coefficient.
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.
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.
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.
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.
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.
The lv2_descriptor()
function is the entry point to the plugin library. The
host will load the library and call this function repeatedly with increasing
indices to find all the plugins defined in the library. The index is not an
indentifier, the URI of the returned descriptor is used to determine the
identify of the plugin.
This method is in the “discovery” threading class, so no other functions or methods in this plugin library will be called concurrently with it.
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.
midigate.ttl
The same set of namespace prefixes with two additions for LV2 extensions this plugin uses: atom and urid.
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.
midigate.c
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.
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.
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.
This plugin also has no extension data to return.
Fifths
This plugin demonstrates simple MIDI event reading and writing.
manifest.ttl.in
fifths.ttl
fifths.c
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
metro.ttl
Since this port supports time:Position, the host knows to deliver time and tempo information
metro.c
During execution this plugin can be in one of 3 states:
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.
The activate() method resets the state completely, so the wave offset is zero and the envelope is 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.
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.
Update the current position based on a host message. This is called by run() when a time:Position is received.
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).
sampler.ttl
sampler.c
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.
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.
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.
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.
Define a macro for converting a gain in dB to a coefficient.
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.
Output audio for a slice of the current cycle.
sampler_ui.c
Set Cairo color to a GDK color (to follow Gtk theme).
atom_sink.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.
Dereference counterpart to atom_sink().
peaks.h
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.
Map URIs used in the peaks protocol.
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()
.
Prepare to start a new peaks transmission. After this is called, the peaks
can be sent with successive calls to peaks_sender_send()
.
Forge a message which sends a range of peaks. Writes a peaks:PeakUpdate
object to forge
, like:
Initialise a peaks receiver. The receiver stores an array of all peaks, which is updated incrementally with peaks_receiver_receive().
Clear stored peaks and free all memory. This should be called when the peaks are to be updated with a different audio source.
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.
uris.h
Write a message like the following to forge
:
Write a message like the following to forge
:
Get the file path from obj
which is a message like:
Simple Oscilloscope
This plugin displays the waveform of an incoming audio signal using a simple GTK+Cairo GUI.
This plugin illustrates:
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
Mono plugin variant
Stereo plugin variant
Gtk 2.0 UI
examploscope.c
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.
Port Indices
Instantiate Method
Connect Port Method
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:
Run Method
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.
Plugin Descriptors
examploscope_ui.c
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)
Representation of the raw audio-data for display (min | max) values for a given index position.
Send current UI settings to backend.
Notify backend that UI is closed.
Notify backend that UI is active.
The plugin should send state and enable data transmission.
Gtk widget callback.
Gdk drawing area draw callback.
Called in Gtk’s main thread and uses Cairo to draw the data.
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
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.
For more information, see https://wiki.xiph.org/Videos/Digital_Show_and_Tell http://lac.linuxaudio.org/2013/papers/36.pdf and https://github.com/x42/sisco.lv2
Called via port_event() which is called by the host, typically at a rate of around 25 FPS.
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.
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
params.ttl
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:
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.
The plugin must list all parameters that can be written (e.g. changed by the user) as patch:writable:
Similarly, parameters that may change internally must be listed as patch:readable, meaning to host should watch for changes to the parameter’s value:
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.
params.c
Helper function to unmap a URID if possible.
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.
State restore method.
state_map.h
Entry in an array that serves as a dictionary of properties.
Comparator for StateMapItems sorted by URID.
Helper macro for terse state map initialisation.
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);
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.