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 |
---|---|---|
a |
lv2:Plugin |
|
lv2:binary |
<amp.so> |
|
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,
¬e_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:
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, ... ] .
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, &, 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.
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
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,
&_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);
}