LV2 programming for the complete idiot
This is a LV2 plugin programming guide for the complete idiot using a set of C++ classes. If you are not a complete idiot, you may want to read the LV2 spec and figure it out for yourself.
This guide assumes some familiarity with the C++ programming tools used on GNU/Linux (GCC, Make etc). Before you start reading you should also install the lv2-c++-tools package. It contains all the libraries and headers you will need to build the code examples. You can get it here.
This is revision 34 of this document.
Copyright © 2007-2010 Lars Luthman. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
If you have any comments or questions about this guide, send them to mail@larsluthman.net. You can get my PGP key here or at any other public key server. The fingerprint is D46B 33AF FFB0 6B32 408E C9CA 6100 9649 44BB B2B3.
Contents |
A completely idiotic plugin
Just to show the things you need to do to create a working plugin we are going to start with a plugin with a single audio output port that always outputs silence. It's completely useless but it will still have all the parts of a complete LV2 plugin.
LV2 plugins always come in bundles. A bundle is a directory with a name that ends with '.lv2'. It can contain any number of plugins, but typically there will only be one plugin per bundle - it's easier to manage them that way.
A plugin consists of a shared library that contains the plugin code and some text files with RDF data that describes the plugin. If you are not a complete idiot you may want to read up on RDF here, otherwise a short introduction follows.
RDF data
RDF is a way to store information as very simple relations. A chunk of RDF data is basically a relational database with one single table that looks like this:
Subject | Predicate | Object |
---|---|---|
Resource | Resource | Resource or literal |
Resource | Resource | Resource or literal |
Resource | Resource | Resource or literal |
... | ... | ... |
A resource is a unique string that can represent pretty much anything - a webpage, a physical object, an abstract concept, a relationship between other resources. These unique strings are written in the form of URIs (if you are not a complete idiot etc: RFC 2396). An example of an URI is a web address (e.g. http://ll-plugins.nongnu.org/) - there are other types but we will only use web URIs in this guide.
When you define a new resource you need to make sure that the URI really is unique, i.e. that no one else is using it to represent something else which might cause problems for LV2 hosts. To do that you should only use web addresses with domain names that you control and that you will be controlling for the forseeable future. Also, the web addresses that you use as your unique URIs don't have to point to an actual web page - the only thing that is important is that they are unique.
A literal is simply a string, integer or floating point constant.
RDF is only an abstract data model, it doesn't have any one particular syntax for text representations. LV2 plugins use a syntax called Turtle. It looks like this:
<http://some.resource/> <http://some.other.resource/foo> <http://yet.another.resource/> . <http://some.resource/> <http://some.other.resource/bar> 5.46 .
The whitespace is not important, it's just to make this example easier to read. In this case the subject is http://some.resource/ in both triples (a subject-predicate-object line is called a triple), the predicates are http://some.other.resource/foo and http://some.other.resource/bar, and the objects are http://yet.another.resource/ and 5.46. This is also an example of a very bad choice of URIs - I do not control the domain names in the URIs and I have no idea if they even exist.
There is also some syntactic sugar that we can use. If you add @prefix sor: <http://some.other.resource/>. at the top of the file you can replace every URI that begins with http://some.other.resource/ with the text sor: followed by the rest of that URI. Also, if you have several triples with the same subject you can write the subject once and use a ; instead of a ., and then that subject will be used for the next triple as well so you only have to write the predicate and the object. The example above is equivalent to
@prefix sor: <http://some.other.resource/>. <http://some.resource/> sor:foo <http://yet.another.resource/>; sor:bar 5.46.
Turtle also has square brackets that you can use when you don't really need a unique identifier for a resource, but still need to use it as both subject and object in different triples. This code
@prefix sor: <http://some.other.resource/>. sor:foo sor:owns [ a sor:Thing; sor:weight 500; sor:colour "red"; ].
is equivalent to
@prefix sor: <http://some.other.resource/>. sor:foo sor:owns sor:nameless. sor:nameless a sor:Thing. sor:nameless sor:weight 500. sor:nameless sor:colour "red".
...except that sor:nameless doesn't actually have an URI at all in the first example - it is called a blank node.
In this example we also used the special Turtle keyword a which is short for http://www.w3.org/2000/01/rdf-schema#type, which means that the subject resource in the triple (sor:nameless or the blank node) is of the type given by the object (sor:Thing).
And finally, you can use , to group triples that have the same subject and predicate but different objects. This snippet
@prefix sor: <http://some.other.resource/>. sor:Lars sor:likes sor:Apples. sor:Lars sor:likes sor:Bananas. sor:Lars sor:likes sor:Oranges. sor:Lars sor:hates sor:Aubergines.
is equivalent to
@prefix sor: <http://some.other.resource/>. sor:Lars sor:likes sor:Apples, sor:Bananas, sor:Oranges; sor:hates sor:Aubergines.
Now you know all the RDF and Turtle you need to write LV2 plugins.
The RDF data for an LV2 bundle should be in a file called manifest.ttl. The manifest.ttl for our silence plugin will look like this:
manifest.ttl:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/silence> a lv2:Plugin; lv2:binary <silence.so>; doap:name "Silence"; doap:license <http://usefulinc.com/doap/licenses/gpl>; lv2:port [ a lv2:AudioPort, lv2:OutputPort; lv2:index 0; lv2:symbol "output"; lv2:name "Output"; ].
Every plugin must have its own URI, and the URI for this plugin is http://ll-plugins.nongnu.org/lv2/lv2pftci/silence. The following 4 lines declare that this resource (our plugin) is of the type lv2:Plugin, has a shared library with the filename silence.so, is known under the name Silence and is licensed under the GNU GPL. These 4 properties are mandatory for an LV2 plugin - if a plugin does not have all of them a host might not load it.
The rest of the file describes the plugin's input and output ports. This plugin only has one single port with the name Output, the symbol output, the index 0 and the types lv2:AudioPort and lv2:OutputPort. Every port must have all these properties, every port symbol must be unique and a valid C identifier, and the indices must start at 0 and be contiguous - you can't have a port with index 4 unless you also have ports with indices 3, 2, 1 and 0. The indices will be used in the shared library to identify the ports.
That's all there is to the RDF data for this simple plugin. Let's move on to the interesting bits.
Code
Since you are supposed to know C++ already, let's just look at the code:
silence.cpp:
#include <lv2plugin.hpp> using namespace LV2; class Silence : public Plugin<Silence> { public: Silence(double rate) : Plugin<Silence>(1) { } void run(uint32_t nframes) { for (uint32_t i = 0; i < nframes; ++i) p(0)[i] = 0; } }; static int _ = Silence::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/silence");
lv2plugin.hpp contains the declarations of the support classes and functions, all of which are in the LV2 namespace.
class Silence : public Plugin<Silence> {
The base class for LV2 plugins is a template class called LV2::Plugin. It implements sensible default implementations for all functions that a LV2 plugin should provide so you only have to override a few of them to get a complete plugin. The template parameter of Plugin should be the plugin subclass itself - this is a trick called the curiously recurring template pattern. Basically it lets us override member functions in our subclass and have the parent class call these new functions without using actual virtual member functions. This can be useful since it's hard for the compiler to optimise virtual function calls, but easy to optimise calls to non-virtual member functions.
Silence(double rate)
The one function that every plugin class must have is a constructor. It must have the prototype that the one above has, but for this simple plugin we don't have to care about the parameter. The constructor must also call the constructor of the parent class (LV2::Plugin<Silence>) with the number of ports as parameter, in our case 1. LV2::Plugin needs this to initialise it's internal port buffer pointers. For this plugin we don't need to do anything else in the constructor, so we'll leave the function body empty.
void run(uint32_t nframes) {
The other function in the Silence class, and the member function that you will probably want to override in all your plugins, is the run() function. This function is called by the host whenever it wants the plugin to process a block of audio - in this case, generate a block of silence. The parameter is the number of frames that should be processed, and the host will have made sure that all the audio port buffers are large enough to contain this many frames.
The member function p(), provided by LV2::Plugin, returns the port buffer for the port with a given index, in this case 0 (the same index that we used for the "Output" port in manifest.ttl - the indices are the link between the port descriptions in the RDF data and the port buffers in the code), as a float pointer. We simply iterate over the audio frames, from 0 to nframes, and set every audio sample in the port buffer to 0.
static int _ = Silence::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/silence");
The last line calls the static member function Silence::register_class() (inherited from LV2::Plugin<Silence>) to register the class Silence as a LV2 plugin with the given URI. The URI needs to be the same as the one you used for the plugin in the RDF data, otherwise the host won't recognise it when it loads the shared library. Silence::register_class() needs to be called when the library is loaded - you could do it in a global function that has the GCC-specific __attribute__((constructor)) set, or in the constructor for a global object, or as above, in the initialiser for a global variable.
We don't care about the return value of this function, the only reason it returns anything at all is so that it can be used in an initialiser list like in this example.
Building and installing
This is the Makefile for our plugin, it should be pretty self-explanatory:
Makefile:
BUNDLE = lv2pftci-silence.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl silence.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp manifest.ttl silence.so $(BUNDLE) silence.so: silence.cpp g++ -shared -fPIC -DPIC silence.cpp `pkg-config --cflags --libs lv2-plugin` -o silence.so install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) silence.so
You can of course also use more advanced build systems like Automake or SCons, but in this guide we will only use basic Makefiles.
Put manifest.ttl, silence.cpp and Makefile in the same directory, run make, and you should get a LV2 bundle containing the Silence plugin. You can install it to /usr/local/lib/lv2/ by typing make install as root, or copy it to ~/.lv2/ to install it just for yourself.
A somewhat useful plugin
Next: a stereo panner plugin. Two audio input ports (left and right channel), two audio output ports (left and right channel) and two control input ports - one for balance and one for width.
The basics
We will write four files for this plugin:
manifest.ttl:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin; rdfs:seeAlso <stereopanner.ttl>.
stereopanner.ttl:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin; lv2:binary <stereopanner.so>; doap:name "Stereo Panner"; doap:license <http://usefulinc.com/doap/licenses/gpl>; lv2:port [ a lv2:ControlPort, lv2:InputPort; lv2:index 0; lv2:symbol "width"; lv2:name "Width"; ], [ a lv2:ControlPort, lv2:InputPort; lv2:index 1; lv2:symbol "balance"; lv2:name "Balance"; ], [ a lv2:AudioPort, lv2:InputPort; lv2:index 2; lv2:symbol "left_input"; lv2:name "Left input"; ], [ a lv2:AudioPort, lv2:InputPort; lv2:index 3; lv2:symbol "right_input"; lv2:name "Right input"; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 4; lv2:symbol "left_output"; lv2:name "Left output"; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 5; lv2:symbol "right_output"; lv2:name "Right output"; ].
stereopanner.cpp:
#include <lv2plugin.hpp> using namespace LV2; class StereoPanner : public Plugin<StereoPanner> { public: StereoPanner(double rate) : Plugin<StereoPanner>(6) { } void run(uint32_t nframes) { float width = *p(0); float balance = *p(1); width = width < 0 ? 0 : width; width = width > 1 ? 1 : width; balance = balance < -1 ? -1 : balance; balance = balance > 1 ? 1 : balance; for (uint32_t i = 0; i < nframes; ++i) { float mid = (p(2)[i] + p(3)[i]) / 2; float side = (p(2)[i] - p(3)[i]) / 2; p(4)[i] = (mid + width * side) * 2 / (1 + width); p(5)[i] = (mid - width * side) * 2 / (1 + width); if (balance < 0) p(5)[i] *= 1 + balance; else p(4)[i] *= 1 - balance; } } }; static int _ = StereoPanner::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner");
Makefile:
BUNDLE = lv2pftci-stereopanner.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl stereopanner.ttl stereopanner.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp manifest.ttl stereopanner.ttl stereopanner.so $(BUNDLE) stereopanner.so: stereopanner.cpp g++ -shared -fPIC -DPIC stereopanner.cpp `pkg-config --cflags --libs lv2-plugin` -o stereopanner.so install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) stereopanner.so
In the previous plugin we had all the RDF data in manifest.ttl. This is not really a good idea, at least not when the RDF data is more than a few lines, because a host will load and parse the manifest.ttl in every single installed LV2 bundle in order to find out what plugins are available. If the manifest files contain a lot of data this can be very slow.
So instead, we just have the bare minimum in manifest.ttl - the triple <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin, so the host knows that this bundle contains that plugin, and a reference to another RDF file that the host can load if it wants to know more about that plugin.
The actual data in stereopanner.ttl is pretty much the same as in the previous plugin, except that we have more ports now. The port type lv2:ControlPort is used for ports whose buffers will contain one single float. They are used as control parameters.
In the C++ file we call the constructor for LV2::Plugin<StereoPanner> with the parameter 6 since this plugin has 6 ports. The following code in the run() function
float width = *p(0); float balance = *p(1); width = width < 0 ? 0 : width; width = width > 1 ? 1 : width; balance = balance < -1 ? -1 : balance; balance = balance > 1 ? 1 : balance;
copies the values from the control port buffers (that are float arrays with one single element each) to the local variables width and balance, and then makes sure that they are within their expected ranges - [0, 1] for width and [-1, 1] for balance. A LV2 host may send any value to a control input port, and the plugin must not break if it receives unexpected values.
The rest of the run() function is trivial - we compute a mono and a difference signal and use those to generate output with the wanted stereo width and balance.
Put manifest.ttl, stereopanner.ttl, stereopanner.cpp and Makefile in the same directory, run make, and you should get a LV2 bundle containing the Stereo Panner plugin.
More metadata
While the Stereo Panner is now fully functional, it would be nice for a host to be able to get some more information about it. For example it could be useful to know what values make sense to send to the control ports ([0, 1] for the width port and [-1, 1] for the balance port) and what values should be used as defaults, to know that setting balance to -1 means that only the left channel will be heard and setting it to 1 means that only the right channel will be heard, and to have some sort of classification for the plugin - all these things could be shown in a GUI to make things easier for the user. All this information can be provided by adding some more triples to stereopanner.ttl.
We start with the port ranges and default values. If we want the host to know about the ranges of values that the control ports expect we use the predicates lv2:minimum and lv2:maximum and to set a default value we use lv2:default, like this:
lv2:port [ a lv2:ControlPort, lv2:InputPort; lv2:index 0; lv2:symbol "width"; lv2:name "Width"; lv2:minimum 0; lv2:maximum 1; lv2:default 1; ], [ a lv2:ControlPort, lv2:InputPort; lv2:index 1; lv2:symbol "balance"; lv2:name "Balance"; lv2:minimum -1; lv2:maximum 1; lv2:default 0; ],
The lines in green are the new ones. To add "Left" and "Right" labels to the balance port we add some more triples:
[
a lv2:ControlPort, lv2:InputPort;
lv2:index 1;
lv2:symbol "balance";
lv2:name "Balance";
lv2:minimum -1;
lv2:maximum 1;
lv2:default 0;
lv2:scalePoint [ <http://www.w3.org/2000/01/rdf-schema#label> "Left";
<http://www.w3.org/1999/02/22-rdf-syntax-ns#value> -1 ];
lv2:scalePoint [ <http://www.w3.org/2000/01/rdf-schema#label> "Right";
<http://www.w3.org/1999/02/22-rdf-syntax-ns#value> 1 ];
],
or to make it a bit more readable, we can add the lines
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
at the top of the file, and then do this instead:
[
a lv2:ControlPort, lv2:InputPort;
lv2:index 1;
lv2:symbol "balance";
lv2:name "Balance";
lv2:minimum -1;
lv2:maximum 1;
lv2:default 0;
lv2:scalePoint [ rdfs:label "Left"; rdf:value -1 ];
lv2:scalePoint [ rdfs:label "Right"; rdf:value 1 ];
],
And finally we tell the host that the plugin is in the "Mixer" category (a subcategory of "Utility" - see the spec for a full list of categories) by adding this triple object:
<http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner>
a lv2:Plugin, lv2:MixerPlugin;
lv2:binary <stereopanner.so>;
doap:name "Stereo Panner";
doap:license <http://usefulinc.com/doap/licenses/gpl>;
Now the plugin is a lot more userfriendly - the host can map knobs and sliders directly to the ranges of the control ports, start the plugin with sensible default settings, and list it in the proper category.
Keeping code and RDF in sync
The port indices are the link between the port descriptions in the RDF data and the port buffers in the code. When a plugin has more than one port you need to keep track of which port has which index - just using the numbers directly is inconvenient and makes the code hard to read. Also, while you are working on a plugin you will probably change the port ranges, add new ports, remove old ones etc. You could define an enum in the code to associate symbolic names to the port indices, but you'd have to change that every time you edited the RDF file. It would be nice if you could do these changes in the RDF data only and have the code update itself automatically.
The lv2-c++-tools package comes with a program called lv2peg. It can read a Turtle RDF file containing information about a LV2 plugin and generate a C header file with an enum for the port indices, an array with the min and max values for each port, the plugin URI as a string constant and some other things. You can include this generated file in the source code for your plugin, and that way you can use symbolic names for the ports that can be updated automatically when the RDF file changes.
You need to add a triple to stereopanner.ttl to tell lv2peg what prefix to use for the symbols in the header file:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. @prefix ll: <http://ll-plugins.nongnu.org/lv2/namespace#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin, lv2:MixerPlugin; lv2:binary <stereopanner.so>; doap:name "Stereo Panner"; doap:license <http://usefulinc.com/doap/licenses/gpl>; ll:pegName "p";
To generate the header file you run lv2peg stereopanner.ttl stereopanner.peg. This will create a file called stereopanner.peg that you can #include in stereopanner.cpp. Then you can edit the source to use the stuff defined in stereopanner.peg, like this:
#include <lv2plugin.hpp> #include "stereopanner.peg" using namespace LV2; class StereoPanner : public Plugin<StereoPanner> { public: StereoPanner(double rate) : Plugin<StereoPanner>(p_n_ports) { } void run(uint32_t nframes) { float width = *p(p_width); float balance = *p(p_balance); width = width < p_ports[p_width].min ? p_ports[p_width].min : width; width = width > p_ports[p_width].max ? p_ports[p_width].max : width; balance = balance < p_ports[p_balance].min ? p_ports[p_balance].min : balance; balance = balance > p_ports[p_balance].max ? p_ports[p_balance].max : balance; for (uint32_t i = 0; i < nframes; ++i) { float mid = (p(p_left_input)[i] + p(p_right_input)[i]) / 2; float side = (p(p_left_input)[i] - p(p_right_input)[i]) / 2; p(p_left_output)[i] = (mid + width * side) * 2 / (1 + width); p(p_right_output)[i] = (mid - width * side) * 2 / (1 + width); if (balance < 0) p(p_right_output)[i] *= 1 + balance; else p(p_left_output)[i] *= 1 - balance; } } }; static int _ = StereoPanner::register_class(p_uri);
To update stereopanner.peg and rebuild stereopanner.so every time you change the RDF data you need to add a target for it in the Makefile and make stereopanner.so depend on it, like this:
BUNDLE = lv2pftci-stereopanner.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl stereopanner.ttl stereopanner.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp manifest.ttl stereopanner.ttl stereopanner.so $(BUNDLE) stereopanner.so: stereopanner.cpp stereopanner.peg g++ -shared -fPIC -DPIC stereopanner.cpp `pkg-config --cflags --libs lv2-plugin` -o stereopanner.so stereopanner.peg: stereopanner.ttl lv2peg stereopanner.ttl stereopanner.peg install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) stereopanner.so stereopanner.peg
Now you can change the port indices, the max and min values and even the plugin URI in the RDF data and just type make to have everything updated.
Extensions
The LV2 architecture is designed to allow anyone to extend it with new functionality without necessarily breaking compatibility with other plugins and hosts. You can add any triples you want to the RDF data for a plugin as long as you use unique URIs. A host that knows what those URIs mean will know what to do about them, and a host that doesn't know what they mean will simply ignore them and load the plugin anyway, without the extra information provided by those triples. We are going to use two such extensions for the Stereo Panner plugin, the Port Groups extension and the GUI extension.
Port groups
An LV2 audio port contains one single channel of audio. In many situations you deal with stereo sound, or even more channels. It would be nice if there was a way for the host to tell which audio ports in a plugin belong together as a stereo pair. Even in our simple Stereo Panner plugin, which only has two audio inputs and two audio outputs, the host can't know whether these actually are left and right channels in a stereo stream, or something else - maybe a carrier and a modulator or something completely different.
With the Port Groups extension we solve that like this:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. @prefix ll: <http://ll-plugins.nongnu.org/lv2/namespace#>. @prefix pg: <http://ll-plugins.nongnu.org/lv2/ext/portgroups#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/in> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/out> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin, lv2:MixerPlugin; lv2:binary <stereopanner.so>; doap:name "Stereo Panner"; doap:license <http://usefulinc.com/doap/licenses/gpl>; ll:pegName "p"; lv2:port [ a lv2:ControlPort, lv2:InputPort; lv2:index 0; lv2:symbol "width"; lv2:name "Width"; lv2:minimum 0; lv2:maximum 1; lv2:default 1; ], [ a lv2:ControlPort, lv2:InputPort; lv2:index 1; lv2:symbol "balance"; lv2:name "Balance"; lv2:minimum -1; lv2:maximum 1; lv2:default 0; lv2:scalePoint [ rdfs:label "Left"; rdf:value -1 ]; lv2:scalePoint [ rdfs:label "Right"; rdf:value 1 ]; ], [ a lv2:AudioPort, lv2:InputPort; lv2:index 2; lv2:symbol "left_input"; lv2:name "Left input"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/in>; pg:role pg:leftChannel; ]; ], [ a lv2:AudioPort, lv2:InputPort; lv2:index 3; lv2:symbol "right_input"; lv2:name "Right input"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/in>; pg:role pg:rightChannel; ]; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 4; lv2:symbol "left_output"; lv2:name "Left output"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/out>; pg:role pg:leftChannel; ]; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 5; lv2:symbol "right_output"; lv2:name "Right output"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/out>; pg:role pg:rightChannel; ]; ].
Each stereo pair is a port group. In this case we have two groups, one for the input ports and one for the output ports, with the URIs http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/in and http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/out. The group URIs do not have to be prefixed with the plugin URI, but it's a convenient way to create new URIs.
In the data for the different ports we then use the predicates pg:membership, pg:group and pg:role to describe which group the port belongs to and what role it has in that group. The Port Groups extension defines many other types of groups, and anyone is free to define their own, but in this example we only care about pg:StereoGroup.
A GUI
A graphical LV2 host will probably have some way to automatically construct a GUI from basic elements (sliders, knobs etc) for any plugin. Still, a plugin may want to provide its own GUI to show a more intuitive interface or just add some personality. For this plugin which only has two controls we don't really need to write our own GUI but we're going to do it anyway.
With the GUI extension we are going to use we should provide a shared library containing the code for the GUI widget. The host will load the library and create a widget instance, pretty much like it does with the plugin. We will use the gtkmm library which is the C++ wrapper for GTK+.
lv2-c++-tools comes with a static library that contains a template base class for LV2 GUIs. For our GUI code, we will simply inherit that.
stereopanner_gui.cpp:
#include <gtkmm.h> #include <lv2gui.hpp> #include "stereopanner.peg" using namespace sigc; using namespace Gtk; class StereoPannerGUI : public LV2::GUI<StereoPannerGUI> { public: StereoPannerGUI(const std::string& URI) { Table* table = manage(new Table(2, 2)); table->attach(*manage(new Label("Width")), 0, 1, 0, 1); table->attach(*manage(new Label("Balance")), 0, 1, 1, 2); w_scale = manage(new HScale(p_ports[p_width].min, p_ports[p_width].max, 0.01)); b_scale = manage(new HScale(p_ports[p_balance].min, p_ports[p_balance].max, 0.01)); w_scale->set_size_request(100, -1); b_scale->set_size_request(100, -1); slot<void> w_slot = compose(bind<0>(mem_fun(*this, &StereoPannerGUI::write_control), p_width), mem_fun(*w_scale, &HScale::get_value)); slot<void> b_slot = compose(bind<0>(mem_fun(*this, &StereoPannerGUI::write_control), p_balance), mem_fun(*b_scale, &HScale::get_value)); w_scale->signal_value_changed().connect(w_slot); b_scale->signal_value_changed().connect(b_slot); table->attach(*w_scale, 1, 2, 0, 1); table->attach(*b_scale, 1, 2, 1, 2); add(*table); } void port_event(uint32_t port, uint32_t buffer_size, uint32_t format, const void* buffer) { if (port == p_width) w_scale->set_value(*static_cast<const float*>(buffer)); else if (port == p_balance) b_scale->set_value(*static_cast<const float*>(buffer)); } protected: HScale* w_scale; HScale* b_scale; }; static int _ = StereoPannerGUI::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/gui");
lv2gui.hpp is the header file that contains the LV2::GUI template base class (it's a template for some of the reasons as LV2::Plugin is a template, but mostly to be consistent). Our GUI class, StereoPannerGUI, has two functions - one constructor and one public member function called port_event().
StereoPannerGUI(const std::string& URI) {
The constructor for a GUI class must have this prototype. URI is the URI for the plugin this GUI will control (one LV2 GUI class can be used for different plugins, so it has to know which plugin this GUI instance is supposed to control). The body of the constructor creates two labels and two horizontal sliders, connects the "value changed" signals in the sliders to the write_control() member function (which writes control changes to the plugin ports) and puts everything in a table. LV2::GUI inherits Gtk::Bin, so we use add() to make the table the main widget.
void port_event(uint32_t port, uint32_t buffer_size, uint32_t format, const void* buffer) {
port_event() is a virtual member function of LV2::GUI. It is called by the host when the value in a control port changes so the GUI can update its control widgets. It can be used for other types of ports as well so it has to be generic, that's why it takes a buffer size, a format identifier and a buffer pointer instead of just a float. For a control port the buffer size will always be sizeof(float), the format will always be 0 and the buffer pointer will always be a pointer to a float so in this case it's safe to cast it to that.
static int _ = StereoPannerGUI::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/gui");
We need to register the GUI class just like we registered the plugin class. The URI is the URI for this GUI, not the URI for the plugin. In this case we just create an URI by appending /gui to the plugin URI, but you can use any URI as long as it's unique.
That's all there is to the GUI code. Now we need to add a reference to this GUI in the RDF data for the plugin and edit the Makefile so it gets built. We do that with these changes:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. @prefix ll: <http://ll-plugins.nongnu.org/lv2/namespace#>. @prefix pg: <http://ll-plugins.nongnu.org/lv2/ext/portgroups#>. @prefix guiext: <http://lv2plug.in/ns/extensions/ui#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/gui> a guiext:GtkUI; guiext:binary <stereopanner_gui.so>; guiext:requiredFeature guiext:makeResident. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/in> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/out> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner> a lv2:Plugin, lv2:MixerPlugin; lv2:binary <stereopanner.so>; doap:name "Stereo Panner"; doap:license <http://usefulinc.com/doap/licenses/gpl>; ll:pegName "p"; guiext:ui <http://ll-plugins.nongnu.org/lv2/lv2pftci/stereopanner/gui>;
BUNDLE = lv2pftci-stereopanner.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl stereopanner.ttl stereopanner.so stereopanner_gui.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp $^ $(BUNDLE) stereopanner.so: stereopanner.cpp stereopanner.peg g++ -shared -fPIC -DPIC stereopanner.cpp `pkg-config --cflags --libs lv2-plugin` -o stereopanner.so stereopanner_gui.so: stereopanner_gui.cpp stereopanner.peg g++ -shared -fPIC -DPIC stereopanner_gui.cpp `pkg-config --cflags --libs lv2-gui` -o stereopanner_gui.so stereopanner.peg: stereopanner.ttl lv2peg stereopanner.ttl stereopanner.peg install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) stereopanner.so stereopanner_gui.so stereopanner.peg
A synth
Now we're going to write a synth plugin that takes MIDI input and produces stereo sound. The stereo outputs will be two audio ports, just like in the Stereo Panner plugin. The MIDI input will be a bit different. The LV2 specification does not say anything about MIDI, it only defines port types for audio and control data, but since a port type is just an URI we can use an extension that does specify a MIDI port type. It will of course only work in hosts that know about this port type.
The basics
The RDF data will look like this:
manifest.ttl:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep> a lv2:Plugin; rdfs:seeAlso <beep.ttl>.
beep.ttl:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. @prefix ll: <http://ll-plugins.nongnu.org/lv2/namespace#>. @prefix pg: <http://ll-plugins.nongnu.org/lv2/ext/portgroups#>. @prefix ev: <http://lv2plug.in/ns/ext/event#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/out> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep> a lv2:Plugin, lv2:InstrumentPlugin; lv2:binary <beep.so>; doap:name "Beep"; doap:license <http://usefulinc.com/doap/licenses/gpl>; ll:pegName "p"; lv2:port [ a ev:EventPort, lv2:InputPort; lv2:index 0; ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent>; lv2:symbol "midi"; lv2:name "MIDI"; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 1; lv2:symbol "left"; lv2:name "Left"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/out>; pg:role pg:leftChannel; ]; ], [ a lv2:AudioPort, lv2:OutputPort; lv2:index 2; lv2:symbol "right"; lv2:name "Right"; pg:membership [ pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/out>; pg:role pg:rightChannel; ]; ].
The only new things here are the category of the plugin (lv2:InstrumentPlugin) and the port type of port 0 (<http://lv2plug.in/ns/ext/event#EventPort>). This port type URI is used for ports that accept some sort of event input, and the line ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent>; specifies that it accepts MIDI events. The extension defining these URIs also define the binary format of MIDI events and the event port buffer itself, but we don't have to care about that - lv2-c++-tools contains a template class called LV2::Synth, a subclass of LV2::Plugin, that does all the event handling and voice mixing for a simple synth. All we have to do in the code is to implement a voice class and pass some information. The code will look like this:
beep.cpp:
#include <lv2synth.hpp> #include "beep.peg" class BeepVoice : public LV2::Voice { public: BeepVoice(double rate) : m_key(LV2::INVALID_KEY), m_rate(rate), m_period(10), m_counter(0) { } void on(unsigned char key, unsigned char velocity) { m_key = key; m_period = m_rate * 4.0 / LV2::key2hz(m_key); } void off(unsigned char velocity) { m_key = LV2::INVALID_KEY; } unsigned char get_key() const { return m_key; } void render(uint32_t from, uint32_t to) { if (m_key == LV2::INVALID_KEY) return; for (uint32_t i = from; i < to; ++i) { float s = -0.25 + 0.5 * (m_counter > m_period / 2); m_counter = (m_counter + 1) % m_period; p(p_left)[i] += s; p(p_right)[i] += s; } } protected: unsigned char m_key; double m_rate; uint32_t m_period; uint32_t m_counter; }; class Beep : public LV2::Synth<BeepVoice, Beep> { public: Beep(double rate) : LV2::Synth<BeepVoice, Beep>(p_n_ports, p_midi) { add_voices(new BeepVoice(rate), new BeepVoice(rate), new BeepVoice(rate)); add_audio_outputs(p_left, p_right); } }; static int _ = Beep::register_class(p_uri);
Our voice class is BeepVoice. LV2::Voice implements some functions that the main synth class expects to find in the voice class, so it's a good idea to inherit from it. The constructor takes the current samplerate as parameter (the voice needs it to play the right pitch). The rest of the member functions are functions that must be implemented for the synth to work - LV2::Synth will call them in response to incoming MIDI events.
void on(unsigned char key, unsigned char velocity) {
This function is called when the plugin has received a MIDI Note On event and wants to use this voice to play it. We store the key in an internal variable (it will be needed in get_key()) and compute a new oscillator period using the helper function LV2::key2hz() that returns the frequency in Hz for a given MIDI key number. If the synth runs out of free voices this function may be called even if the voice is already playing a note, it should then switch to the new key as fast as possible.
void off(unsigned char velocity) {
This is called when the plugin has received a MIDI Note Off for the key that this voice is playing. Normally you'd probably want to start a smooth fadeout here, but we will just turn off the voice directly.
unsigned char get_key() const {
This is called when LV2::Synth needs to know what key this voice is playing. If it returns LV2::INVALID_KEY it means that the voice is free and can be used to play a new note when the next Note On event is received.
void render(uint32_t from, uint32_t to) {
The function that actually renders the audio. In this example we simply render a square wave with the frequency of the current key (or actually two octaves below the current key - square waves are bright) with the amplitude 0.25, to leave room for a couple of voices to play at the same time without clipping. One important thing to notice here is that we don't write the rendered samples directly to the port buffers, but add them to what's already there. Other voices may already have been rendered in this block and if we wrote directly to the buffer those voices wouldn't appear in the output.
You may wonder how we can use the p() function here to get the port buffers - it is a member function of LV2::Plugin, and our voice class does not inherit LV2::Plugin. The answer is simply that it's another p() function - LV2::Voice has a member function with the same name, and before the voices start processing a new block LV2::Synth will pass them a pointer to its port buffer list. This means that you can access all port buffers in the voice class as well, including control port buffers (though we don't have any of those in this example).
The other class in the source file is Beep. It is the main plugin class, and it inherits LV2::Synth<BeepVoice, Beep> - the first template parameter is the voice class we want to use for this synth plugin, and the second template parameter is the plugin class itself, just like in the earlier plugins. LV2::Synth implements its own run() function. When inheriting LV2::Synth you should not override it with your own run() function unless you really know what you're doing. All we need to do in this class is to call the LV2::Synth constructor with the right parameters (the first is the same as before - the number of ports - and the second parameter is the index of the MIDI input port), add some voices, and tell LV2::Synth which ports are the main audio output ports so it can fill them with zeros before passing them to the voice classes.
And just for completeness' sake, here's the Makefile:
Makefile:
BUNDLE = lv2pftci-beep.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl beep.ttl beep.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp $^ $(BUNDLE) beep.so: beep.cpp beep.peg g++ -shared -fPIC -DPIC beep.cpp `pkg-config --cflags --libs lv2-plugin` -o beep.so beep.peg: beep.ttl lv2peg beep.ttl beep.peg install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) beep.so beep.peg
Put these four files in the same directory and run make, and you should get a synth plugin. When you play it you should hear an annoying squarewave that starts and ends abruptly as you press and release the keys.
A bit more interesting
The synth works, but it sounds rather dull. It would be nice to make it a bit more interesting.
We'll start by giving each new note a random stereo position. We can do that by including the standard header file <cstdlib> and making these changes in the BeepVoice class:
class BeepVoice : public LV2::Voice { public: BeepVoice(double rate) : m_key(LV2::INVALID_KEY), m_rate(rate), m_period(10), m_counter(0) { } void on(unsigned char key, unsigned char velocity) { m_key = key; m_period = m_rate * 4.0 / LV2::key2hz(m_key); m_pos = std::rand() / float(RAND_MAX); } void off(unsigned char velocity) { m_key = LV2::INVALID_KEY; } unsigned char get_key() const { return m_key; } void render(uint32_t from, uint32_t to) { if (m_key == LV2::INVALID_KEY) return; for (uint32_t i = from; i < to; ++i) { float s = -0.25 + 0.5 * (m_counter > m_period / 2); m_counter = (m_counter + 1) % m_period; p(p_left)[i] += (1 - m_pos) * s; p(p_right)[i] += m_pos * s; } } protected: unsigned char m_key; double m_rate; uint32_t m_period; uint32_t m_counter; float m_pos; };
And since we use a square wave some pulse width modulation would be nice. We'll add a new control port for the PWM amount and also make the modulation depend on the key velocity and a linear decay envelope.
First, the description of the PWM port in beep.ttl:
[
a lv2:AudioPort, lv2:OutputPort;
lv2:index 2;
lv2:symbol "right";
lv2:name "Right";
pg:membership [
pg:group <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/out>;
pg:role pg:rightChannel;
];
],
[
a lv2:ControlPort, lv2:InputPort;
lv2:index 3;
lv2:symbol "pwm";
lv2:name "PWM";
lv2:minimum 0;
lv2:maximum 1;
lv2:default 0.5;
].
And the code changes:
class BeepVoice : public LV2::Voice { public: BeepVoice(double rate) : m_key(LV2::INVALID_KEY), m_rate(rate), m_period(10), m_counter(0) { } void on(unsigned char key, unsigned char velocity) { m_key = key; m_period = m_rate * 4.0 / LV2::key2hz(m_key); m_pos = std::rand() / float(RAND_MAX); m_envelope = velocity / 128.0; } void off(unsigned char velocity) { m_key = LV2::INVALID_KEY; } unsigned char get_key() const { return m_key; } void render(uint32_t from, uint32_t to) { if (m_key == LV2::INVALID_KEY) return; for (uint32_t i = from; i < to; ++i) { float pwm = *p(p_pwm) + (1 - *p(p_pwm)) * m_envelope; float s = -0.25 + 0.5 * (m_counter > m_period * (1 + pwm) / 2); m_counter = (m_counter + 1) % m_period; p(p_left)[i] += (1 - m_pos) * s; p(p_right)[i] += m_pos * s; if (m_envelope > 0) m_envelope -= 0.5 / m_rate; } } protected: unsigned char m_key; double m_rate; uint32_t m_period; uint32_t m_counter; float m_pos; float m_envelope; };
It doesn't quite sound like the string section in a symphonic orchestra but it's a little better than before.
Post processing
Often when you write a synth plugin you want to do some processing on the mixed sound of all voices before you send it to the outputs - add some reverb, a global filter, or just adjust the gain. When you inherit from LV2::Synth you can do this by overriding the post_process() member function. It is called by LV2::Synth whenever a block of audio has been rendered and written to the output port buffers. We are going to add a stereo echo effect and a simple gain controlled by a control port.
First the control port, just like before:
[
a lv2:ControlPort, lv2:InputPort;
lv2:index 3;
lv2:symbol "pwm";
lv2:name "PWM";
lv2:minimum 0;
lv2:maximum 1;
lv2:default 0.5;
],
[
a lv2:ControlPort, lv2:InputPort;
lv2:index 4;
lv2:symbol "gain";
lv2:name "Gain";
lv2:minimum 0;
lv2:maximum 2;
lv2:default 1;
].
And then the code. We only need to edit the Beep class this time.
class Beep : public LV2::Synth<BeepVoice, Beep> { public: Beep(double rate) : LV2::Synth<BeepVoice, Beep>(p_n_ports, p_midi), m_buf_pos(0), m_delay(rate / 3), m_l_buffer(new float[m_delay]), m_r_buffer(new float[m_delay]) { add_voices(new BeepVoice(rate), new BeepVoice(rate), new BeepVoice(rate)); add_audio_outputs(p_left, p_right); for (unsigned i = 0; i < m_delay; ++i) { m_l_buffer[i] = 0; m_r_buffer[i] = 0; } } ~Beep() { delete [] m_l_buffer; delete [] m_r_buffer; } void post_process(uint32_t from, uint32_t to) { for (uint32_t i = from; i < to; ++i) { float mono = (p(p_left)[i] + p(p_right)[i]) / 2; p(p_left)[i] += m_l_buffer[m_buf_pos]; p(p_right)[i] += m_r_buffer[m_buf_pos]; float tmp = m_l_buffer[m_buf_pos]; m_l_buffer[m_buf_pos] = 0.6 * (mono + m_r_buffer[m_buf_pos]); m_r_buffer[m_buf_pos] = 0.6 * tmp; m_buf_pos = (m_buf_pos + 1) % m_delay; p(p_left)[i] *= *p(p_gain); p(p_right)[i] *= *p(p_gain); } } protected: unsigned m_buf_pos; unsigned m_delay; float* m_l_buffer; float* m_r_buffer; };
With these changes we get a 0.33 second stereo echo and a new control port that we can use to set the volume of the synth.
A GUI
We would of course also like a GUI for our synth. In real life we would would probably want knobs or sliders to control the PWM and gain parameters, but we saw how to do that in the GUI for the stereo panner - so lets skip that this time, and instead just write a GUI that we can use to send a test tone to the synth.
For this we will need to use something called a mixin. A mixin is a class (in this case a template class) that you pass as an additional template parameter to LV2::Plugin or LV2::GUI to add functions to your plugin class, in this case to add a function for writing MIDI events from our GUI. Let's take a look at the code:
#include <gtkmm.h> #include <lv2gui.hpp> #include "beep.peg" using namespace sigc; using namespace Gtk; class BeepGUI : public LV2::GUI<BeepGUI, LV2::URIMap<true>, LV2::WriteMIDI<true> > { public: BeepGUI(const std::string& URI) : m_button("Click me!") { m_button.signal_pressed().connect(mem_fun(*this, &BeepGUI::send_note_on)); m_button.signal_released().connect(mem_fun(*this, &BeepGUI::send_note_off)); pack_start(m_button); } protected: void send_note_on() { uint8_t event[] = { 0x90, 0x40, 0x40 }; write_midi(p_midi, 3, event); } void send_note_off() { uint8_t event[] = { 0x80, 0x40, 0x40 }; write_midi(p_midi, 3, event); } Button m_button; }; static int _ = BeepGUI::register_class("http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/gui");
The template parameters LV2::URIMap<true> and LV2::WriteMIDI<true> are the mixins mentioned earlier. They are template classes with a single bool parameter. The meaning of this parameter is that if it's true the GUI will refuse to instantiate if the host doesn't support the URI map extension (an extension that is needed by the MIDI extension) or MIDI event writing, and if it's false it will instantiate anyway. Our GUI would be completely useless without MIDI support, so we set it to true.
The constructor simply connects the "pressed" and "released" signals of our button to the member functions send_note_on() and send_note_off(). Let's look at those functions.
void send_note_on() { uint8_t event[] = { 0x90, 0x40, 0x40 }; write_midi(p_midi, 3, event); }
This function first declares an array of three uint8_t elements, and initialises it to [0x90 0x40 0x40] - a note on event for MIDI key 48. It then calls write_midi(), which is the function that was added by the mixin LV2::WriteMIDI. This function takes three parameters; port number, event size, and event data. In our case we send the 3-byte MIDI event to the p_midi port.
send_note_off() is identical except that it sends a note off event instead of a note on (status byte 0x80 instead of 0x90).
Just like with the GUI for the stereo panner plugin we need to add a reference to this one in the RDF data file for the plugin:
@prefix lv2: <http://lv2plug.in/ns/lv2core#>. @prefix doap: <http://usefulinc.com/ns/doap#>. @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>. @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>. @prefix ll: <http://ll-plugins.nongnu.org/lv2/namespace#>. @prefix pg: <http://ll-plugins.nongnu.org/lv2/ext/portgroups#>. @prefix ev: <http://lv2plug.in/ns/ext/event#>. @prefix guiext: <http://lv2plug.in/ns/extensions/ui#>. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/gui> a guiext:GtkUI; guiext:binary <stereopanner_gui.so>; guiext:requiredFeature guiext:makeResident; guiext:requiredFeature guiext:Events. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/out> a pg:StereoGroup. <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep> a lv2:Plugin, lv2:InstrumentPlugin; lv2:binary <beep.so>; doap:name "Beep"; doap:license <http://usefulinc.com/doap/licenses/gpl>; ll:pegName "p"; guiext:ui <http://ll-plugins.nongnu.org/lv2/lv2pftci/beep/gui>; ...
The only difference here is that we also add the required GUI feature guiext:Events. That tells the host that it should not try to load our GUI unless it supports sending events from the GUI to the plugin.
Finally, we add a target for the GUI in our Makefile:
BUNDLE = lv2pftci-beep.lv2 INSTALL_DIR = /usr/local/lib/lv2 $(BUNDLE): manifest.ttl beep.ttl beep.so beep_gui.so rm -rf $(BUNDLE) mkdir $(BUNDLE) cp $^ $(BUNDLE) beep.so: beep.cpp beep.peg g++ -shared -fPIC -DPIC beep.cpp `pkg-config --cflags --libs lv2-plugin` -o beep.so beep_gui.so: beep_gui.cpp beep.peg g++ -shared -fPIC -DPIC beep_gui.cpp `pkg-config --cflags --libs lv2-gui` -o beep_gui.so beep.peg: beep.ttl lv2peg beep.ttl beep.peg install: $(BUNDLE) mkdir -p $(INSTALL_DIR) rm -rf $(INSTALL_DIR)/$(BUNDLE) cp -R $(BUNDLE) $(INSTALL_DIR) clean: rm -rf $(BUNDLE) beep.so beep_gui.so beep.peg
That will build a GUI for the Beep synth plugin that can get the synth to produce test tones.