Cleaning Up the LV2 Extension Mess
After reading my last post, and watching a few old talks around LV2 and so on, I got to thinking about the extension mess problem I mentioned, and it occured to me that there might be some commonality here with the "staging" or "contrib" area question as well.
This is all based on some ideas that have been bouncing around in my head for ages, but that I haven't really developed and certainly not written down, so I'm going to try and sketch out a proposal for how to handle these things without breaking anything.
Concretely, there are two problems here: one is that the spec is just a mess. For example, the Data Access and Instance Access extensions are really just parts of the same thing and should live together, nobody cares about Morph and it's not in a state that really belongs in the "recommended standard" list (sorry, flagrant abuse of power on my part there), and so on.
The other problem is that there are sometimes contributions which solve a problem, and are a reasonable enough pragmatic step, but also not really up to par. Maybe they aren't portable, aren't defined well enough, could do more harm than good if they're presented as recommendations, and so on. People, for whatever reason, want them "in LV2". Yet, nobody has the time to spend to develop them into a more proper specification yet, and nobody is happy when things don't get merged.
It seems there is a common factor to these problems, and it's moving things without breaking anything. To clean up the current mess, we can move extensions to the contrib area. When a previously half-baked contribution is developed further, we can move it from the contrib area. This is an obvious coarse-grained use case; I think there is also a case for finer-grained URI migration, but I'll focus on the easy and most useful case for now.
How might we do this? Though moving instance-access to contrib is not a goal, it's about as simple as an extension gets, so I'll pretend we want to do that for the sake of a simple example. At the very least, it will be a nice little fantasy for me to pretend that the curse of crappy plugin UIs that mess with DSP guts has finally been vanquished for good :) This is just about the mechanism, what we should actually do to clean things up is a question for another time.
So, what's instance-access? It's a handful of URIs, and a feature. The feature is extremely simple, the payload is just some pointer. Can those URIs be moved without breaking anything? For at least this simple case, I think so:
Lilv, on loading data, aggressively maps everything to the new location. If it says http://lv2plug.in/ns/ext/instance-access in a data file, then it gets loaded into memory as http://lv2plug.in/ns/contrib/instance-access. In this case, that means that, as far as the host can tell, the UI has lv2:optionalFeature http://lv2plug.in/ns/contrib/instance-access. This can be done pretty easily just above the parser level so that it's universally true.
When the UI is instantiated, the (old) host passes the http://lv2plug.in/ns/ext/instance-access feature to
lilv_plugin_instantiate(). Internally, lilv duplicates this, and passes both the old and new features to the UI with the same data.
The plugin is either old, and looks for the old feature URI, or new, and looks for the new feature URI, and either way, finds it.
I can't think of a reason this wouldn't work, and it doesn't even require any host changes. It's a bit bloated, but not in a way that matters, and would need a significant (but not too bad) amount of code specifically to deal with this in lilv, but such is my lot in life.
In the more general case, there is also the issue of URID mappings. Let's pretend that http://lv2plug.in/ns/ext/instance-access is mapped to a URID both by the host and the plugin, and that URID is sent between them. Though this isn't really an intended use-case for this particular extension, it's a perfectly valid thing to do:
- The host URID-map maps both the old and new URIs to the same URID.
... that's it, actually. Regardless of which "version" either host or plugin know about, the URID is identical. This requires hosts to actually implement something though, or for a URI map to be added to lilv, so it's not as easy. It can't just be done in LV2 and would take some time to get established.
There is one remaining snag:
extension_data. This one is a bit trickier,
because we need to assume the hosts uses
which is just a trivial wrapper, and probably not used by everyone. That's an
easy enough fix to make, though. Then, lilv just needs to call the plugin
method for the new URI, return that if it isn't NULL, and fallback to calling
it with the old URI.
All of this requires a map between old and new to exist, of course, but this would be written down in the specs themselves and it's easy enough to load such a thing inside lilv.
I'm sure there are other places where URIs as strings are used in the API that would need thinking about, and I'll have to scan through the spec to see, but I suspect the above is at least 90% of what matters.
So... am I missing something? Do send me (or lv2-dev) an email if so, but now that I write it down this seems more viable than I assumed it would be. There will definitely be corner cases, since plugins and hosts can use these strings for anything everywhere, but as far as the actual interface is concerned it seems possible to make this happen without too much pain. What could we do with this?
Merge data-access and instance-access
Merge buf-size and resize-port
Put all the "official" extensions in the same namespace ("directory"), and get rid of the annoying inconsistency of
extensionsand so on (which doesn't really matter, except in the soft sense that ugliness matters). The header includes already look like this and it's so much nicer.
We could put the deprecated extensions in a special namespace so they really stand out, but this doesn't seem to really matter (though it should be done visually on the spec page regardless).
Move presets into lv2core itself? This isn't an extension-level move like the above, but why not? One less prefix to bother with, and in retrospect, a plugin spec without any kind of presets at all is pretty silly. Perhaps the same for port-groups.
Do... something with port-properties, and maybe parameters. Let's say combine them into a "control" extension that generally has all the definition of control related stuff.
Move morph to contrib.
Maybe move dyn-manifest to contrib. This is a bit more contentious, but it's a pretty ugly solution, and the caveats of using it currently aren't very clear.
That would leave a specification list like this (assuming parameters and port-properties move to "control"):
- Atom: A generic value container and several data types
- Buf Size: Access to, and restrictions on, block and buffer sizes
- Instance Access: Provides access to a plugin instance
- Log: A feature for writing log messages
- LV2: An open and extensible audio plugin standard
- MIDI: A normalised definition of raw MIDI
- Options: Instantiation time options
- Control: Common properties and parameters for audio processing
- Patch: Messages for accessing and manipulating properties with events
- State: An interface for LV2 plugins to save and restore state
- Time: Properties for describing time
- UI: LV2 plugin UIs of any type
- Units: Meaningful units for values
- URID: Features for mapping URIs to and from integers
- Worker: Support for doing non-realtime work in plugins
Not everything left is immaculate, and from a user-facing documentation point of view other things like putting the data-only vocabularies in a separate section might help even more, but I think this would be a big improvement. More importantly, it would of course give us an attic to put slightly more sketchy things. Looking at LV2 as a Specification™, that feels wrong, but looking at it as a project, it seems really necessary.
LV2: The good, bad, and ugly
It occurred to me that I haven't really been documenting what I've been up to, a lot of which is behind the scenes in non-release branches, so I thought I would write a post about the general action around LV2 lately. I've also been asked several times about what the long-term strategy for LV2 is, if there should be an "LV3", whether LV* can start to really gain traction as a competitor to the big proprietary formats, and so on.
So, here it is, a huge brain dump on what's good, what's bad, what's ugly, and what I think should be done about it.
LV2 is different from other plugin standards in several ways. This is not always a good thing (which we'll get to shortly), but there are some things that have proven to be very good ideas, even if the execution was not always ideal:
Openness: Obvious, but worth mentioning anyway.
Extensibility: The general idea of building an extensible core, so that plugin and host authors can add functionality in a controlled way is a great one. This allows developers to prototype new functionality to eventually be standardised, make use of additional functionality if it is available, and so on. Some problems, like ensuring things are documented, that implementations agree, and so on, get more difficult when anybody can add anything, but this is worth the benefit of not having a standardisation process block getting things done.
DSP and UI split: Also obvious in my opinion, but certainly not a universal thing. There are a lot of bad things to be said about the actual state of GUI support, but keeping them separate, with the option to have a pointer to the guts of a plugin instance is the right approach. Having a well-defined way to communicate between GUI and DSP makes it easy to do the right thing. Multi-threaded realtime programming is hard, and plugins dropping out because of GUI activity and so on should not be a thing.
Standard implementation between host and plugins (for some things): This is a huge win in reducing the burden on both host and plugin authors, and allows both to rely on certain things being done right. This also makes a location where stronger validation and so on can happen, which we should exploit more. The war between host and plugin authors, trying to make things compatible with the arbitrary behaviour of countless implementations is largely why everyone hates plugins. This doesn't have to be a thing. We haven't actually done well in that area with LV2 (quite the opposite), but having a place to put that code is the right move.
Transparent communication: Though you technically can do just about anything with LV2, a "proper" plugin has a transparent control interface that works in a standard way. This gets you all kinds of things for free, like human-readable debug tracing, network transparency, and so on, and also encourages design that's better from a user point of view, like having good host controls for parameters, automation, accessibility, and so on. This is somewhat related to having a DSP and UI split. The benefits of having plugins be controlled in a standard way are endless, as are the awful things that happen when GUIs and audio code aren't forcefully kept at arm's reach.
Now to the more interesting part. There are some nice ideas in LV2, and I think an idealised and cleaned up version of it that adheres to the main underlying design principles would be beautiful. In reality, however, LV2 is an atrocious mess in all kinds of ways:
Control ports: LV2 uses LADSPA-style control ports, which contain a single float. This is a tricky one to put in the "bad" category, since pragmatically grafting extensibility onto LADSPA is why LV2 has been moderately successful. It had to be that way: we needed working plugins, not a tedious standardisation process that goes nowhere (there's already GMPI for that). That said, control ports are incredibly limiting and that they still exist is an endless source of trouble: they are static, they require buffer splitting for sample accuracy, they can only convey a float, there is no hook to detect changes and do smoothing, and so on. A control protocol (something like MIDI except... good) is the right way to control plugins. Notes and controls and all the rest should be in the same stream, synchronous with audio. It's hard to migrate to such a reality, but there should be one consistent way to control a plugin, and it should be a stream of sample-accurate events. No methods, no threading and ABI nightmares, no ambiguity, just a nice synchronous stream of inputs, and a single run function that reads those and produces outputs.
connect_portmethod: Another LADSPA-ism. This means that using some signal means the host must call a method on the plugin to connect it first. This is an awful design: it forces both the host and the plugin to maintain more state than is necessary, and it's slow. I have written several plugins that would be completely stateless (essentially pure functions) except the spec requires the plugin to maintain all these pointers and implement methods to mutate them. Inputs and outputs just should be passed to the run method, so all of that goes away and everything is nicely scoped. As far as the basics of the C API are concerned, this is, in my opinion, the most egregious mistake.
Turtle: Everyone loves to hate Turtle. It's mostly a nice syntax (if the namespace prefix limitations are very annoying), but it's weird. Worse, people might search for "RDF" and find the confusing W3C trash-fire there. The underlying ideas are good, but that three-letter-acronym should be absolutely eliminated from the spec and documentation. The good thing in LV2 is really just "property-centric design", which can be explained in a simple way anyone can understand. It's more or less just "JSON with URIs" anyway, and nobody ever got fired for using JSON. Speaking of which, syntax-wise, JSON-LD is probably the way to go today. JSON is annoying in different ways, but this would allow LV2 data files to look completely typical to almost any developer, but still have the same meaning and have the same advantages under the hood of a real data model. This could actually be done without breaking anything in practice, but JSON-LD is much harder to implement so I'm not quite there yet. It would also be some work to write the vocabulary (vocabularies?), but it's doable.
Lack of quality control: Again a consequence of pragmatic evolution, but the lack of standard quality control has become a real problem. There has been progress made there, with things like
lv2_validate, but it's not good enough. The biggest problem with plugins (and plugin hosts) in general is that most of them are just broken. There should be a standard test suite for both, that is as strict as possible, and its use should be strongly "encouraged" at the very least. The above-mentioned existence of standard code in-between hosts and plugins could be useful here, for example, hosts could just refuse to load non-conforming plugins outright.
Extension spam: The "standard" extensions are not all very good, or widely supported. They also aren't broken down and organized especially well in some cases. We are at least somewhat stuck with this for compatibility, but it makes things confusing. There are many reasons for this, but in general I think a better thought-out standardisation process, and a "sort of standard" staging ground to put contributions that some implementations agree on but aren't ideal or quite at "recommended standard" yet would help. I'm still not sure exactly how to do this, there's no best practice for such things out there that's easy to steal, but with the benefit of hindsight I think we could do much better.
Library spam: The standard host implementation is quite a few libraries. This is a mostly good thing, in that they have distinct purposes, different dependencies, and so on, but in practice it's annoying for packagers, or anyone who wants to vendor it. I think the best approach here is to combine them into a meta-package or "SDK", so libraries can still be properly split but without the maintenance burden. I am working towards this with "lv2kit". It's currently hard for outsiders to even figure out what they need, a one-stop "all the LV2 things" in a single package would help immensely, especially for people outside of the Linux world (where distributions package everything anyway, so nobody really cares).
C++ and other language bindings: Plugin interfaces more or less have to be in C. However, outside of POSIXland, nobody wants to actually write C. Virtually the entire audio industry uses C++. Good bindings are important. Python is also nice for some things. Rust would be great, and so on.
These are things that are just... well, ugly. Not really "bad" in concrete ways that matter much, but make life unpleasant all the same.
Extensibility only through the URI-based mechanism: In general, extensibility is good. The host can pass whatever features, and plugins can publish whatever interfaces, and everything is discoverable and degrades gracefully and so on. It works. The downside is that there's some syntactic overhead to that which can be annoying. We should have put sizes or versions in structs so they were also extensible in the classical way. For example, the
connect_portproblem mentioned above could be fixed by adding a new run method, but we can't literally add a new run method to
LV2_Descriptor. We would have to make a separate interface, and have the host access it with
extension_data, and so on, which makes things ugly. Maybe this is for the best, but ugliness matters. In general there are a few places where we could have used more typical C patterns. Weirdness matters too.
Extension organization: The list of specifications is a complete mess. It annoys me so much. I am not really sure about this: in some cases, an extension is a clearly separate thing, and having it be essentially a separate spec is great. In other cases, we've ended up with vaguely related grab-bags of things for lack of anywhere else to put them. I sometimes wonder if the KISS approach of just having one big namespace would have been the right way to go. It would mean less prefixes everywhere at the very least. Maybe we could use some other way of grouping things where it makes sense?
Static data: This is a tough one. One of the design principles of LV2 is that hosts don't need to load and run any code to just discover plugins, and information about them. This is great. However, whenever the need for something more dynamic comes along (dynamic ports, say), we don't have any great way to deal with it, because the way everything is described is inherently static. Going fully dynamic doesn't feel great either. I think the solution here is to take advantage of the fact that the data files are really just a syntax and the same data can be expressed in other ways. We already have all the fundamental bits here, Atoms are essentially "realtime-ready RDF" and can be round-tripped to Turtle without loss. My grand, if vague, vision here is that everything could just be the same conceptually, and the source of it be made irrelevant and hidden behind a single API. For example, a data file can say things like (pseudocode alert)
<volume> hasType Float; <volume> minimumValue 0.0; <volume> maximumValue 1.0but a message from a plugin can say exactly the same thing at run time. If the host library (lilv) handled all this nicely, hosts could just do
lv2_get_minimum(gain)and not really care where the information came from. I think this is a much better approach than grafting on ever-more API for every little thing, but it would have to be done nicely with good support. I think the key here is to retain the advantages we have, but put some work into making really obvious and straightforward APIs for everything.
Overly dynamic URIDs: URIDs are a mechanism in LV2 where things are conceptually URIs (which makes everything extensible), but integers in practice for speed. Generally a URID is made at instantiation time by calling a host-provided mapping function. This is, for the most part, wonderful, but being always dynamic causes some problems. You need dynamic state to talk about URIs at all, which makes for a lot of boilerplate, and gets in the way of things like language bindings (you couldn't make a simple standalone template that gives you an
Intatom for an
int32_t, for example). I think it would be a good idea to have a static set of URIDs for things in the standard, so that
lv2_minimumor whatever is just statically there, but preserve the ability to extend things with dynamic mapping. This is easy enough by adding the concept of a "minimum dynamic URID value", where everything less than that is reserved by the standard. Alternatively, or perhaps in addition, maybe having a standard loader to ease the pain of loading every little thing (like with OpenGL) would help make code cleaner and boilerplate free.
The Documentation Sucks: Of course, the documentation of everything always sucks, so you have to take this feedback with a grain of salt, but it's true of LV2. A lot of improvements here are blocked by the specification breakdown being set in stone, but it could be improved. I think the reference documentation is not the problem though, we really need example-driven documentation written as prose. This is a completely different thing to reference documentation and I think it's important to not confuse the two. There has been a bit of work adapting the "book" to be better in this sense, but it's not very far along. Once it's there, it needs to be brought to the forefront, and the reference documentation put in a place where it's clear it's about details. Optics matter.
I'm sure there are countless things floating around in my mind I've forgotten about at the moment, but that's all that comes to mind at a high level. There are, of course, countless little specific problems that need work (like inventing a control protocol for everything, and having it be powerful but pleasant to use), but I'm only focusing on the greater things about LV2 itself, as a specification family and a project. The big question, of course, is whether LV3 should be a thing. I am not sure, it's a hard question. My thinking is: maybe, but we should work towards it first. It's always tempting to throw out everything and Do It Right, but that never works out. The extensible nature of LV2 means that we can graft better things on over time, until all the various pieces feel right. I see no point in breaking the entire world with a grandiose LV3 project until, for example, we've figured out how we want to control plugins. I am a big believer in iterative design, and working code in general. We can build that in LV2. Maybe we can even do it and end up at more or less LV3 anyway, without causing any hard breakage. To that end, I have been improving things in general, to try and address some of the above, and generally bring the software up to a level of quality I am happy with:
Portability: The LV2 host stack has (almost) always been at least theoretically portable, and relatively portable in practice, but it's obvious that it comes from the Linux world and might work elsewhere. I have been doing a lot of work on the DevOps front to ensure that everything works everywhere, always, and no platform is second-class. The libraries live on Gitlab, and have a CI setup that builds and tests on Linux (both x86 and ARM), Windows, and MacOS, and cross-compiles with MinGW.
Frequent releases: Another consequence of the many-libraries problem is that releasing is really tedious, and I'm generally pretty bad at making releases. This makes things just feel stale. I've recently almost entirely automated this process, so that everything involved in making a release can be done by just calling a script. Also on the DevOps and stale fronts, I've been moving to automatically generating documentation on CI, so it's always published and up to date. Automating everything is important to keep a project vibrant, especially when maintenance resources are scarce.
Generally complex APIs: The library APIs aren't great, and the general situation is confusing. Most authors only need Lilv, but there are these "Serd" and "Sord" things in there that show up sometimes, all work with roughly the same sort of "nodes", but all have different types and APIs for them, and so on. I have been working on a new major version of serd that takes advantage of the API break to make things much simpler, and improve all kinds of things in general. This will be exposed directly in lilv where it makes sense, eliminating a lot of glue, and eliminating the sord library entirely. The lilv API itself is also dramatically bigger and more complicated than it needs to be. At the time, it felt like adding obvious helper methods for every little thing was a good idea, so people can just find
lv2_port_get_specific_thing_I_want()which is nice when it's there... except it's not always there. The property-based design of LV2 means that
lv2_get(port, specific_thing_I_want)could work for everything (and this ability is already there). This results in situations like people thinking they are blocked by a missing function, and spending a lot of time writing and submitting patches to add them, when the functionality was there all along. It would be easier on everyone if everything just always worked the same general way, and it would make the API surface much smaller which is always nice.
Validation: There has been a data validator for a while, but it wasn't great. It didn't, for example, point at the exact position in the file where the error was, you just had to figure that part out. The new version of serd fixes this, so validation errors and warnings use standard GCC format to report the exact position along with a helpful error message, which automatically integrates with almost every editor or IDE on the planet for free.
SDK: As mentioned above, I'm working on putting all the "standard" host libraries into a unified "lv2kit" which is the one package you will need to build LV2 things. There are still some details about this I haven't sorted out (e.g. should the spec be in there or not? What about non-LV2-specific libraries like serd? Optional vendoring?), but it's coming along and I think will make it far more realistic to expect people to implement LV2.
The spec mess: I am idly thinking about whether or not it would be possible to add a compatibility mechanism to allow us to move URIs without breaking anything. It's largely superficial, but cleaning up the specification list would really help the optics of the project if nothing else. 90% here is trivial (just aggressively map everything forwards), but all the corner cases still need to be thought out.
That's all the work in the trenches going on at the moment to improve the state of LV2. Though I wish I, or anyone else, had the time and energy to invest effort into addressing the more ambitious questions around the plugin API itself, at the moment I am more than tapped out. Regardless, I think it makes sense to get the current state of things in a form that is moving forward and easier to work with, and raise the quality bar as high as possible first. With a very high-quality implementation and extensive testing and validation, I'll feel a lot more confident in addressing some of the more interesting questions around plugin interfaces, and perhaps someday moving towards an LV3.
On that note, feedback is always welcome. Most of the obvious criticism are well-known, but more perspectives are always useful, and silent wheels get no grease. Better yet, issues and/or merge requests are even more welcome. The bus factor of LV2 isn't quite as bad as it seems from the web, but it would help to get more activity on the project itself from anyone other than myself. The standards for API additions and such are pretty high, but there's plenty of low-hanging fruit to be picked.
Fomp.lv2 1.2.0 has been released. Fomp is an LV2 port of the MCP, VCO, FIL, and WAH plugins by Fons Adriaensen.
- Add missing default values
- Add reverb and reverb_amb
- Adjust some control ranges and defaults to more sensible values
- Bump plugins to stable versions
- Fix a few misspelled port symbols (breaks state compatibility)
- Fix phaser plugin data and list phasers in manifest
- Remove invalid reference to old LADSPA pulse_vco binary
MDA.lv2 1.2.4 has been released. This is a port of the MDA VST plugins to LV2.
- Fix a few minor metadata issues
- Fix initial noise with Piano (zero comb filter and voices on startup)
- Fix invalid Vocoder preset description
- Fix misleading indentation warnings with GCC 6
- Update build system
- Use rdfs:label for port groups
Jalv 1.6.4 has been released. Jalv is a simple but fully featured LV2 host for Jack which exposes plugin ports to Jack, essentially making any LV2 plugin function as a Jack application. For more information, see http://drobilla.net/software/jalv.
- Support rdfs:label for port groups
- Use screen refresh rate with Gtk3 and Qt5
- Add more strict error detection when storing plugin state properties
- Add option to override LV2_PATH in applications
- Don't print errors when saving state if correct links already exist
- Fix GCC8 warnings
- Fix creating directories across drives on Windows
- Fix issues with loading state with saved files from the model
- Fix memory errors and Python 3.4+ compatibility in Python bindings
- Fix unit tests on Windows
- Make Python bindings more Pythonic
Sratom 0.6.4 has been released. Sratom is a small library for serialising LV2 atoms to and from RDF, for converting between binary and text or storing in a model. For more information, see http://drobilla.net/software/sratom.
- Make sratom_free() safe to call on NULL
- Various minor code cleanups
Jalv 1.6.2 has been released. Jalv is a simple but fully featured LV2 host for Jack which exposes plugin ports to Jack, essentially making any LV2 plugin function as a Jack application. For more information, see http://drobilla.net/software/jalv.
- Add jalv -i option to ignore stdin for background use
- Add several commands to console interface
- Add support for running as an internal Jack client (thanks Timo Wischer)
- Add support for underscore in port names on command line (thanks Jośe Fernando Moyano)
- Fix Jack deactivation
- Fix compilation with recent Gtkmm versions that require C++11
- Fix potential crash when closed with worker (thanks JP Cimalando)
- Fix potential hang after Ctrl-c in console interface (thanks Laxmi Devi)
- Make Suil dependency optional
- Remove support for deprecated event and uri-map extensions
Page 1 / 9 »