diff options
Diffstat (limited to 'doc/lv2_plugin_guide')
-rw-r--r-- | doc/lv2_plugin_guide/guide.adoc | 622 |
1 files changed, 622 insertions, 0 deletions
diff --git a/doc/lv2_plugin_guide/guide.adoc b/doc/lv2_plugin_guide/guide.adoc new file mode 100644 index 0000000..df085a0 --- /dev/null +++ b/doc/lv2_plugin_guide/guide.adoc @@ -0,0 +1,622 @@ +Programming LV2 Plugins +======================= +:toc: +:source-highlighter: pygments + + +Introduction +------------ + +The link:http://lv2plug.in/ns[LV2 specification] is a large collection of documentations and extensions to +the core LV2 API. +On their own they're difficult to parse. +In this document a collection of example plugins are presented to show how the +components of LV2 fit together and demonstrate best practices when writing LV2 +plugins. + +Overall this guide should be considered a complement for the more complete LV2 +specification. +The examples presented will correspond with those included in the LV2 +distribution and will be presented in order of increasing complexity. + +This includes: + +A simple amplifier:: Shows the basic LV2 skeleton, introduces control ports, +provides basic .ttl file information +A MIDI gate:: Shows midi processing, introduces LV2 atoms, introduces LV2 +extension API +Fifths:: A more complex MIDI processor, explains ??? LV2 concept +Metromome:: Explores timining information, explains ??? LV2 concept +A trivial sampler:: Provides a MIDI waveform sampler, introduces the LV2 worker +extension and extensions in general +A GUI oscilloscope:: Discusses user interfaces in the context of the LV2 +ecosystem + +This manual encourages users to jump around, but recommends reading the LV2 +asides in each chapter in order to best understand how the LV2 specific +concepts relate to each other. + +Building a basic amplifier +-------------------------- + +In this example we're going to build a simple amplifier plugin. +There will be audio input, audio output, and a gain control. + +[ditaa] +-------------------------------------------------------------------------------- + + Gain In + | + | + v + +-------------+ + | | +Audio In ----> | Amp | ------> Audio Out + | | + +-------------+ +-------------------------------------------------------------------------------- + + +Part 1: The code +~~~~~~~~~~~~~~~~ + +Let's start off by including the core LV2 header. + +[source, C] +-------------------------------------------------------------------------------- +#include <lv2/lv2plug.in/ns/lv2core/lv2.h> +-------------------------------------------------------------------------------- + +The include path of LV2 headers reflect the URI of the specification they are a part of by design. +This makes it easy to find detailed information about the API. +This header is part of the "core" LV2 specification: http://lv2plug.in/ns/lv2core. + +LV2 plugins are are responsible for keeping track of what ports they have and what host-controlled data structures they're attached to. +In our simple amplifier, these are the only variables stored in the plugin structure: + +[source, C] +---- +include::../../plugins/eg-amp.lv2/amp.c[tags=Amp] +---- + +We also define an `enum` for their indices to keep the code readable: + +[source, C] +---- +include::../../plugins/eg-amp.lv2/amp.c[tags=PortIndex] +---- + +With the data for our amplifier set up we need to implement the functions needed +for the host to communicate with our plugin. +These functions are summarized in the link:http://lv2plug.in/doc/html/group__lv2core.html#structLV2__Descriptor[LV2_Descriptor] which is passed to a +host and it includes: + +//More details in later examples +// Actually the LV2 spec for these should have a bunch more detail on each, +// though perhaps it should still be integrated + +instantiate:: Allocate and initialize the plugin. +connect_port:: Connect a port to a host-provided buffer. +activate:: Reset the plugin to a default state and prepare it to run. +run:: Process data for a given number of samples. +deactivate:: Stop the plugin from running (counterpart to activate). +cleanup:: Free plugin (counterpart to instantiate). +extension_data:: Get data defined by LV2 extensions. + +This plugin does not need the `activate`, `deactivate`, or `extension_data` functions, which are optional and do not need to be defined if they are not needed. + +NOTE:: For this simple plugin we don't really need to care about when each one +of these callbacks can be called. For more detailed information about what +guarantees you have for when each callback may be invoked see XXX + +Since this plugin contains minimal state, the `instantiate` and `cleanup` functions simply allocate and free the `Amp` structure: + +[source, C] +-------------------------------------------------------------------------------- +include::../../plugins/eg-amp.lv2/amp.c[tags=instantiate] +-------------------------------------------------------------------------------- + +[source, C] +-------------------------------------------------------------------------------- +include::../../plugins/eg-amp.lv2/amp.c[tags=cleanup] +-------------------------------------------------------------------------------- + +Connecting ports provided by the LV2 host is somewhat more complex. +Each call from the host will provide a pointer to a buffer for one of the ports. +The plugin will need to store that pointer and either read from it or write to +it depending upon if it's an input or an output. + +[source, C] +---- +include::../../plugins/eg-amp.lv2/amp.c[tags=connect_port] +---- + +After the host has connected data to each one of the ports and activated the +plugin then the amplfier LV2 plugin can run for a number of samples. + +[source, C] +---- +include::../../plugins/eg-amp.lv2/amp.c[tags=run] +---- + +All that's left is providing the host with a descriptor containing pointers to +all of these functions. +Since multiple LV2 plugins can be contained in the same shared library this is +done through the exported lv2_descriptor(link) function: + +[source, C] +---- +include::../../plugins/eg-amp.lv2/amp.c[tags=descriptor] +---- + +Part 2: The specification +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we have an amplifier plugin setup, you might expect that a host could +load it and be ready to run. +The code however doesn't define all of the details. +For instance the code doesn't provide a list of the inputs and outputs of the +plugin, so how does the host know about them? + +The answer is that LV2 uses data files to store this information. + + +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 http://www.w3.org/TeamSubmission/turtle/[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. + + +A .ttl file maps a collection of properties to values using a collection of +URLs. +Now let's build one for the amplifier called 'amp.ttl' + +We can start out by saying that the AMP_URL which we previously defined is 'a' +plugin: + +[source, turtle] +-------------------------------------------------------------------------------- +<http://lv2plug.in/plugins/eg-amp> + a http://lv2plug.in/ns/lv2core#Plugin . +-------------------------------------------------------------------------------- + +Since we're going to define a number of properties in the LV2 namspace, let's +define a few macros to condense the file and since we're defining multiple +properties on our plugin let's use the continuation ';' rather than the property +terminator '.'. + +[source, turtle] +-------------------------------------------------------------------------------- +include::../../plugins/eg-amp.lv2/amp.ttl[tags=prefixes] +-------------------------------------------------------------------------------- + +Now we can begin describing our plugin, by first stating that it is 'a' LV2 plugin: + +[source, turtle] +-------------------------------------------------------------------------------- +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin ; +-------------------------------------------------------------------------------- + +IMPORTANT: The URI used to identify the plugin in the data must match the one in the code, `AMP_URI` in this example. + +From here we can provide information to what project the plugin is associated +with, a display name, a license, and even specify features of the plugin: +[source, turtle] +-------------------------------------------------------------------------------- + lv2:project <http://lv2plug.in/ns/lv2> ; + doap:name "Simple Amplifier" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:optionalFeature lv2:hardRTCapable ; +-------------------------------------------------------------------------------- + +Now as per defining the inputs and outputs of the plugin, let's consider what +information we need to specify. + +- Type of data +- Input vs output +- Name of the port for a host to display +- Index in the C code +- etc + +For the control port it could also be handy to state the default value, a +minimum, maximum, and what sort of units are present. +All of this information helps hosts display the state of the plugin. + +With these basics in mind we have the following port description: + +[source, turtle] +-------------------------------------------------------------------------------- +include::../../plugins/eg-amp.lv2/amp.ttl[tags=ports] +-------------------------------------------------------------------------------- + +Part 3: The bundle +~~~~~~~~~~~~~~~~~~ + +Phew, that's a lot of setup, but we're almost there. +All that's left is creating a bundle for the audio plugin and describing the +contents in a manifest .ttl. +What's a bundle exactly? +Well a bundle is a directory that includes: + +- The plugin's executable library +- One ttl file per plugin in the library to define ports and describe features + about the plugin +- One manifest .ttl file providing metadata about what ttl files a host needs to + look at + +This manifest file makes searching through your plugin library a quick and +efficient process for plugin hosts and as such you're expected to have a tiny +manifest. +For this project it is: + +[source, turtle] +-------------------------------------------------------------------------------- +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin ; + lv2:binary <amp.so> ; + rdfs:seeAlso <amp.ttl> . +-------------------------------------------------------------------------------- + +In that manifest we define that: + +- The bundle only contains one plugin +- The URI of that plugin +- The files that the host needs to use the plugin. + + +Part 4: A recap +~~~~~~~~~~~~~~~ + +amp.c: +[source, C] +-------------------------------------------------------------------------------- +#include <lv2/lv2plug.in/ns/lv2core/lv2.h> +#define AMP_URI "http://lv2plug.in/plugins.eg-amp" + +typedef struct { + const float *gain; //control input + const float *data_in; //audio input + float *data_out; //audio output +} Amp; + +typedef enum { + AMP_GAIN, + AMP_INPUT, + AMP_OUTPUT +} PortIndex; + +static void activate(LV2_Handle instance) {} +static void deactivate(LV2_Handle instance) {} +static const char *extension_data(const char *uri) { return NULL; } + +static LV2_Handle instantiate(const LV2_Descriptor *, +double, const char *, const LV2_Features**) { return calloc(sizeof(Amp)); } +static void cleanup(LV2_Handle instance) { free(instance) }; + +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; + } +} + +#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f) + +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 i = 0; i < n_samples; ++i) + output[i] = input[i] * coef; +} + +static const LV2_Descriptor descriptor = { + AMP_URI, + instantiate, + connect_port, + activate, + run, + deactivate, + cleanup, + extension_data +}; + +LV2_SYMBOL_EXPORT +const LV2_Descriptor* +lv2_descriptor(uint32_t index) +{ + switch (index) { + case 0: return &descriptor; + default: return NULL; + } +} +-------------------------------------------------------------------------------- + +amp.ttl: +[source, turtle] +-------------------------------------------------------------------------------- +@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#> . +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin ; + lv2:project <http://lv2plug.in/ns/lv2> ; + doap:name "Simple Amplifier" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:optionalFeature lv2:hardRTCapable ; + lv2:port [ + a lv2:InputPort , lv2:ControlPort ; + lv2:index 0 ; + lv2:symbol "gain" ; + lv2:name "Gain" + lv2:default 0.0 ; + lv2:minimum -90.0 ; + lv2:maximum 24.0 ; + units:unit units:db ; + ] , [ + 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" + ] . +-------------------------------------------------------------------------------- + +manifest.ttl: +[source, turtle] +-------------------------------------------------------------------------------- +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . + +<http://lv2plug.in/plugins/eg-amp> + a lv2:Plugin ; + lv2:binary <amp.so> ; + rdfs:seeAlso <amp.ttl> . +-------------------------------------------------------------------------------- + +That's the first LV2 plugin example out of the way. +In this chapter we introduced the code skeleton needed to run a LV2 plugin, the +necessary supporting metadata files, and how they can be bundled together to +form a distributable bundle. +This is just the basics of the LV2 API and we'll start to get a bit more in +depth with the next example which starts processing MIDI events. + +Building a MIDI Gate +-------------------- + +Now that the basic skeleton is out of the way, let's talk about a slightly more +complex control data in the form of MIDI. +Sticking with the same core functionality of an amplifier, let's build a new +plugin that either outputs the input signal or outputs a sequence of zeros +depending upon the current MIDI input. + +[ditaa] +-------------------------------------------------------------------------------- + + MIDI (LV2 atom) + | + | + v + +-------------+ + | | +Audio In ----> | Gate | ------> Audio Out + | | + +-------------+ +-------------------------------------------------------------------------------- + + +Part 1: What's an Atom? +~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of starting with the code for this new plugin, let's take a look at how +the '.ttl' files will differ. +The manifest will be nearly identical once the plugin name is changed. +The midigate.ttl file will feature a replacement for the old control port +looking like: + +[source,turtle] +-------------------------------------------------------------------------------- + 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" + ] , [ +-------------------------------------------------------------------------------- + +We've got a new type of port which features LV2 atoms. +A http://lv2plug.in/ns/ext/atom[LV2 Atom] is essentially a flexible container +for all sorts of different data. It could be floats, strings, arrays, etc. +In this case we're specifying that it uses +http://lv2plug.in/ns/ext/midi/[MIDI] by specifying 'lv2:supports'. +Lastly we provide a hint to the host that the primary (and currently only) +control input is this port by specifying the 'lv2:destination'. + +Part 1b: What's an Atom URID? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both 'midi' and 'atom' are in the extension namespace of LV2, but they're +generally considered part of the core set of features a host should provide. +Going beyond them, there is the http://lv2plug.in/ns/ext/urid[URID] extension +which makes using Atom ports a bit easier. +So, let's include that as well: + +[source,turtle] +-------------------------------------------------------------------------------- + lv2:requiredFeature urid:map ; +-------------------------------------------------------------------------------- + +Part 2: Handling Atoms in C +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A large bit of this plugin is going to still use the same skeleton as the first +example, so let's focus on what is different. +To start off with, we're going to need headers for each one of the chunks of the +API we're using: + +[source,C] +-------------------------------------------------------------------------------- +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#define MIDIGATE_URI "http://lv2plug.in/plugins/eg-midigate" +-------------------------------------------------------------------------------- + +Next up we'll want to store our data which consists of: + +- The port bindings +- A URID which identifies a MIDI event from other Atom events +- The number of active notes + +[source, C] +-------------------------------------------------------------------------------- +typedef struct { + const LV2_Atom_Sequence* control; + const float* in; + float* out; + + LV2_URID midi_MidiEvent; + + unsigned n_active_notes; +} Midigate; +-------------------------------------------------------------------------------- + +Now in instantiate we can find the value of the MIDI event URID: + + +-------------------------------------------------------------------------------- +static LV2_Handle +instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature* const* features) +{ + //Search the provided feature list for the requested URID mapper + LV2_URID_Map* map = NULL; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + break; + } + } + + //A host MUST provide the required features + assert(map); + + Midigate* self = (Midigate*)calloc(1, sizeof(Midigate)); + //Store the URID for a midi event + self->uris.midi_MidiEvent = map->map(map->handle, LV2_MIDI__MidiEvent); + + return (LV2_Handle)self; +} +-------------------------------------------------------------------------------- + +Now to handle the actual MIDI events. +Within the run() function we can step through all of the events in the control +port and handle them in-order. +What this means for the output port is that we can process a chunk of samples up +to the new MIDI event, update the number of notes, and then process the next +chunk of data. + +[source, C] +-------------------------------------------------------------------------------- +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Midigate* self = (Midigate*)instance; + size_t offset = 0; + + LV2_ATOM_SEQUENCE_FOREACH(self->control, ev) { + write_output(self->out + offset, self->in + offset, + self->n_active_notes > 0, + ev->time.frames - offset); + + + if (ev->body.type == self->uris.midi_MidiEvent) { + + const uint8_t* const msg = (const uint8_t*)(ev + 1); + + switch (lv2_midi_message_type(msg)) { + case LV2_MIDI_MSG_NOTE_ON: ++self->n_active_notes; break; + case LV2_MIDI_MSG_NOTE_OFF: --self->n_active_notes; break; + } + } + + + offset = ev->time.frames; + } + + write_output(self->out + offset, self->in + offset, + self->n_active_notes > 0, + sample_count - offset); +} + +static void +write_output(float *dst, const float *src, bool active, size_t len) { + if (active) + memcpy(dest, src, len * sizeof(float)); + else + memset(dest, 0, len * sizeof(float)); +} +-------------------------------------------------------------------------------- + +Now the last bit of housekeeping. +When we first initialize the plugin all of the data is zeroed out, but that +isn't the case if the host deactivates the plugin and reruns the 'activate()' +function. +We need to preserve any connections, but wipe out any state in the plugin. +For the MIDI gate this is just clearing the number of active notes + +[source, C] +-------------------------------------------------------------------------------- +static void activate(LV2_Handle instance) { + Midigate* self = (Midigate*)instance; + self->n_active_notes = 0; +} +-------------------------------------------------------------------------------- + + +Part 3: MIDI Gate Wrapup +~~~~~~~~~~~~~~~~~~~~~~~~ + +In review... |