From 7b67c423a084898a2bc2f6569285550c0b6c3961 Mon Sep 17 00:00:00 2001 From: fundamental Date: Sun, 27 May 2018 23:12:15 -0400 Subject: Add initial WIP for more user friendly docs See May 2018 lv2-devel discussion for some of the motivations for the different style of documentation. --- doc/lv2_host_guide/lv2-host-perspective.adoc | 185 ++++++++ doc/lv2_plugin_guide/guide.adoc | 622 +++++++++++++++++++++++++++ plugins/eg-amp.lv2/amp.c | 27 +- plugins/eg-amp.lv2/amp.ttl | 4 + 4 files changed, 834 insertions(+), 4 deletions(-) create mode 100644 doc/lv2_host_guide/lv2-host-perspective.adoc create mode 100644 doc/lv2_plugin_guide/guide.adoc diff --git a/doc/lv2_host_guide/lv2-host-perspective.adoc b/doc/lv2_host_guide/lv2-host-perspective.adoc new file mode 100644 index 0000000..c890c54 --- /dev/null +++ b/doc/lv2_host_guide/lv2-host-perspective.adoc @@ -0,0 +1,185 @@ +Introduction to LV2, a Plugin Host Perspective +============================================== +:toc: + +Intro +----- + +Hello there, if you're reading this document then you're likely trying to figure +out how to work with existing LV2 plugins and understanding some of the details +of the LV2 spec. +This document assumes some cursory knowledge about prior plugin standards +including LADSPA(link)/DSSI(link)/VST(link)/etc. +At the end of reading this guide you should be comfortable with setting up a +basic host which can process audio, MIDI, and control events using several of +the LV2 utility libraries in C. +If you're looking for a C++ implementation, we'll briefly explain how the +information within this guide can be translated at the end of this article. + +For the most part in this guide we'll refer to the official API specifications +for more clarifications, though several code examples will come from one of +several simple hosts including jalv(link) which is the reference implementation, +lv2file(link) (a simple wav file processor), lv2apply(), lv2ls(link), and ... + +What can LV2 plugins do? +------------------------ + +talk about the raw basics of audio synthesis, audio effects, midi effects, and +GUIs. +Have a brief mention of extensions, but do *NOT* talk about them until later. + +Perhaps mention the existance of .ttl files here and say xxx is abstracted away +by yyy. + + +The LV2 Ecosystem +----------------- + +If you're searching around for libraries related to LV2 you may come across a +wide variety of them. +This can include lilv(links), raul(for), suil(every), serd(one), sord(of the), +LV2 toolkit(libraries), and a few others. +Fortunately building a basic host is not that difficult and will primarily build +off of lilv. + +Talk about the other components as need be + +How can you load a LV2 plugin? +------------------------------ + +So, now that we know that we're using lilv, how can we load a simple plugin. +For this let's try to load one of the basic sample plugins 'eg-amp'. +Plugins within LV2 are loaded based upon URIs, so in the case of the 'eg-amp' +plugin LV2 names it 'http://lv2plug.in/plugins/eg-amp'. +(perhaps a brief footnote on why this is the case, but nothing more) + +First let's verify that this plugin is installed on your system + +-------------------------------------------------------------------------------- +void list_plugins(void) +{ + LilvWorld *world = lilv_world_new(); + lilv_world_load_all(world); + const LilvPlugins *plugins=lilv_world_get_all_plugins(lilvworld); + + LilvIter *plug_itr = lilv_plugins_begin(plugins); + for(int i=0; i | Amp | ------> Audio Out + | | + +-------------+ +-------------------------------------------------------------------------------- + + +Part 1: The code +~~~~~~~~~~~~~~~~ + +Let's start off by including the core LV2 header. + +[source, C] +-------------------------------------------------------------------------------- +#include +-------------------------------------------------------------------------------- + +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] +-------------------------------------------------------------------------------- + + 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] +-------------------------------------------------------------------------------- + + 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 ; + doap:name "Simple Amplifier" ; + doap:license ; + 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: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . +-------------------------------------------------------------------------------- + +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 +#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: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . + + a lv2:Plugin ; + lv2:project ; + doap:name "Simple Amplifier" ; + doap:license ; + 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: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . +-------------------------------------------------------------------------------- + +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... diff --git a/plugins/eg-amp.lv2/amp.c b/plugins/eg-amp.lv2/amp.c index c3ba279..433a9d4 100644 --- a/plugins/eg-amp.lv2/amp.c +++ b/plugins/eg-amp.lv2/amp.c @@ -36,17 +36,21 @@ 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. */ +// tag::AMP_URI[] #define AMP_URI "http://lv2plug.in/plugins/eg-amp" +// end::AMP_URI[] /** In code, ports are referred to by index. An enumeration of port indices should be defined for readability. */ +// tag::PortIndex[] typedef enum { AMP_GAIN = 0, AMP_INPUT = 1, AMP_OUTPUT = 2 } PortIndex; +// end::PortIndex[] /** Every plugin defines a private structure for the plugin instance. All data @@ -54,12 +58,13 @@ typedef enum { every instance method. In this simple plugin, only port buffers need to be stored, since there is no additional instance data. */ +// tag::Amp[] typedef struct { - // Port buffers - const float* gain; - const float* input; - float* output; + const float* gain; // Gain control input + const float* input; // Audio input + float* output; // Audio output } Amp; +// end::Amp[] /** The `instantiate()` function is called by the host to create a new plugin @@ -71,6 +76,7 @@ typedef struct { This function is in the ``instantiation'' threading class, so no other methods on this instance will be called concurrently with it. */ +// tag::instantiate[] static LV2_Handle instantiate(const LV2_Descriptor* descriptor, double rate, @@ -81,6 +87,7 @@ instantiate(const LV2_Descriptor* descriptor, return (LV2_Handle)amp; } +// end::instantiate[] /** The `connect_port()` method is called by the host to connect a particular @@ -90,6 +97,7 @@ instantiate(const LV2_Descriptor* descriptor, This method is in the ``audio'' threading class, and is called in the same context as run(). */ +// tag::connect_port[] static void connect_port(LV2_Handle instance, uint32_t port, @@ -109,6 +117,7 @@ connect_port(LV2_Handle instance, break; } } +// end::connect_port[] /** The `activate()` method is called by the host to initialise and prepare the @@ -124,6 +133,8 @@ activate(LV2_Handle instance) { } +// tag::run[] + /** 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) @@ -149,6 +160,8 @@ run(LV2_Handle instance, uint32_t n_samples) } } +// end::run[] + /** 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 @@ -171,11 +184,13 @@ deactivate(LV2_Handle instance) This method is in the ``instantiation'' threading class, so no other methods on this instance will be called concurrently with it. */ +// tag::cleanup[] static void cleanup(LV2_Handle instance) { free(instance); } +// end::cleanup[] /** The `extension_data()` function returns any extension data supported by the @@ -193,6 +208,8 @@ extension_data(const char* uri) return NULL; } +// tag::descriptor[] + /** Every plugin must define an `LV2_Descriptor`. It is best to define descriptors statically to avoid leaking memory and non-portable shared @@ -228,3 +245,5 @@ lv2_descriptor(uint32_t index) default: return NULL; } } + +// end::descriptor[] diff --git a/plugins/eg-amp.lv2/amp.ttl b/plugins/eg-amp.lv2/amp.ttl index 9f522a1..87fd76a 100644 --- a/plugins/eg-amp.lv2/amp.ttl +++ b/plugins/eg-amp.lv2/amp.ttl @@ -2,11 +2,13 @@ # `manifest.ttl`. This is done so the host only needs to scan the relatively # small `manifest.ttl` files to quickly discover all plugins. +# tag::prefixes[] @prefix doap: . @prefix lv2: . @prefix rdf: . @prefix rdfs: . @prefix units: . +# end::prefixes[] # 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 @@ -36,6 +38,7 @@ "Просто Усилитель"@ru ; doap:license ; lv2:optionalFeature lv2:hardRTCapable ; +# tag::ports[] 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. @@ -88,3 +91,4 @@ lv2:symbol "out" ; lv2:name "Out" ] . +# end::ports[] -- cgit v1.2.1