JUCE LV2 Plugin Wrapper

[quote=“jpo”]Nice ! I’ll have to try that.

Which host to you recommend, for testing ?[/quote]
Jalv (http://drobilla.net/software/jalv/) is very light and small, and has all possible lv2 features (developed by drobilla ;))

I had your LV2 wrapper set up and running in just one hour, VERY impressive ! My plugin seems to work fine, the gui shows up in jalv. The presets do not work for me, though. It seems you are not only saving the state chunk but also parameters values. I think this is redundant, for a VST plugin, as the state contains everything, and modifyng individual parameters after setting the state is unnecessary (and could maybe cause the plugin to perform unnecessary computations, or diverge from its preset state). It my case it also caused jalv to complain about missing preset ports , probably because the name of some parameters of my plugin depend on its current state

As I’m building a common VST/LV2 binary, in order to get the wrapper to build, I had to put all you wrapper code (except the exported functions) in a namespace juceLV2 { } to avoid name conflicts (SharedMessageThread etc)

I had also to remove this code , which is not used anyway:

__attribute__((constructor)) void myPluginInit() {} __attribute__((destructor)) void myPluginFini() {}

Regarding the manifest generation , did you consider the ‘dynamic manifest’ extension ?

If your plugin needs custom-state save, please implement the getStateInformationString() function instead of using binary chunks please.
This require a change in the juce AudioProcessor code, but it’s for the best.

Here’s an example using the juce-demo plugin:

[code]void JuceDemoPluginAudioProcessor::setStateInformationString (const String& data)
{
XmlElement* const xmlState = XmlDocument::parse(data);

if (xmlState != 0)
{
    // make sure that it's actually our type of XML object..
    if (xmlState->hasTagName ("MYPLUGINSETTINGS"))
    {
        // ok, now pull out our parameters..
        lastUIWidth  = xmlState->getIntAttribute ("uiWidth", lastUIWidth);
        lastUIHeight = xmlState->getIntAttribute ("uiHeight", lastUIHeight);

        gain  = (float) xmlState->getDoubleAttribute ("gain", gain);
        delay = (float) xmlState->getDoubleAttribute ("delay", delay);
    }
    delete xmlState;
}

}

String JuceDemoPluginAudioProcessor::getStateInformationString ()
{
// Create an outer XML element…
XmlElement xml (“MYPLUGINSETTINGS”);

// add some attributes to it..
xml.setAttribute ("uiWidth", lastUIWidth);
xml.setAttribute ("uiHeight", lastUIHeight);
xml.setAttribute ("gain", gain);
xml.setAttribute ("delay", delay);

return xml.createDocument (String::empty);

}[/code]

if you implement this and set ‘JucePlugin_WantsLV2StateString’, then you can use it for only the extra non-parameter data.
using the state lv2 extension to save parameter values is not allowed!
the plugin is only suppose to know about his own parameter values during run()

maybe I misunderstoo what you wrote, but you can’t change the number parameters in LV2 (static data, remember?).
If that is required, then simply don’t export any parameters.
dynmanifest is not properly supported everywhere, and it’s kind of a hack… even if I used it, you still wouldn’t get on-the-fly parameter changes

the constructor/destructor were there in the vst wrapper (which I based for doing this work), so I left them alone.

I can pass a base64 encoded state to getStateInformationString , but what does that change ? It will be basically what you are doing with filter->getCurrentProgramStateInformation(chunkMemory); const String chunkString = Base64Encode(chunkMemory); except that it will be performed on my side. The state content will be still completely opaque.

No I’m not changing the number of parameters, only their names. It does not cause trouble with vst/au/rtas hosts because they use the parameter index instead of the name when dealing with automation data or when setting parameter values. So maybe if I change the lv2:symbol values in the plugin ttl file to a generic “parameter#num” it won’t cause any error (the correct parameter name will still not be updated but that is a minor issue)

[quote=“jpo”]I can pass a base64 encoded state to getStateInformationString , but what does that change ? It will be basically what you are doing with filter->getCurrentProgramStateInformation(chunkMemory); const String chunkString = Base64Encode(chunkMemory); except that it will be performed on my side. The state content will be still completely opaque.[/quote]
If you need binary data, then of course don’t use strings. you can put a macro in your code to only save non-parameter data if LV2.
btw, I just made a git commit that fixed presets.ttl chunk generation, please update

[quote=“jpo”]
No I’m not changing the number of parameters, only their names. It does not cause trouble with vst/au/rtas hosts because they use the parameter index instead of the name when dealing with automation data or when setting parameter values. So maybe if I change the lv2:symbol values in the plugin ttl file to a generic “parameter#num” it won’t cause any error (the correct parameter name will still not be updated but that is a minor issue)[/quote]
LV2 use string symbols for parameters and not indexes. Since Juce (following any other standard) use indexes, I trick juce code by converting parameter names into proper symbols (and making sure they don’t repeat, they must be unique).
not sure why jalv is failing though, symbols/names are only used on ttl generation, anywhere else (at runtime) it uses indexes.

One note though - because lv2-presets are host-side and the plugin can’t do nothing about it, I had to create my own lv2 extension for plugin-side midi-mappable programs (it was quite a discussion…).
Latest Qtractor (SVN) supports this and not the official lv2-presets, so as my host Carla (unfinished). The lv2-programs spec is here - http://kxstudio.sourceforge.net/ns/lv2ext/programs/
On the LV2 wrapper I use both lv2-presets and lv2-programs :smiley:

With your latest change, the presets are now working correctly in jalv ! My issue with parameter names was solved by replacing the “if (trimmedName.isEmpty())” in nameToSymbol by a simple “if (true)” – that way my lv2 parameter ports have are identified by a generic symbol with is independent of the actual name.

Awesome!
I look forward to test one of your plugins! :twisted:

Btw here is what I’m using on linux when the plugin wants to find out what is the full pathname of its .so file (could be a replacement for getBinaryName, but that’s not very important):

void dummyFunction() {} ... Dl_info inf; memset(&inf, 0, sizeof inf); int er=dladdr((void*)&dummyFonction, &inf); if (er != 0) { strncpy(path, inf.dli_fname, 512); path[511] = 0; } else strcpy(path, JucePlugin_Name);

Sure ! I’ll send you a copy as soon as I have packaged it in a usable way

Btw the presets.ttl generation is a bit slow with my plugin, it takes ~30 seconds to generate it. replacing

for (int i = 0; i < numPrograms; i++) { presets += "<" JucePlugin_LV2URI "#preset" + String(i+1) + "> a pset:Preset ;\n"; presets += " rdfs:label \"" + filter->getProgramName(i) + "\" ;\n"; presets += ....etc; }

with

[code]
for (int i = 0; i < numPrograms; i++)
{
String preset;
preset += “<” JucePlugin_LV2URI “#preset” + String(i+1) + “> a pset:Preset ;\n”;
preset += " rdfs:label “” + filter->getProgramName(i) + “” ;\n";
preset += …etc;

    presets += preset;

}[/code]

brings it back to ~1 second.

FYI: if you want to do a lot of string concatenations, it’s much faster to use << into a MemoryOutputStream and then get the string from it afterwards using toString().

Here is my local patch for now. The hostPrograms variable is not initialized in the constructor, it caused some random crashes when changing preset. I also had to add a few MessageManagerLock in the constructor / destructor / setStateBinary / setStateString to avoid assertions in juce component code.

[code]— distrho/libs/juce-lv2/juce_LV2_Wrapper.cpp 2012-05-22 17:49:27.000000000 +0200
+++ lv2/juce_LV2_Wrapper.cpp 2012-05-23 12:27:42.000000000 +0200
@@ -100,10 +100,12 @@
#define PLUGIN_EXT “.dll”
#endif

+namespace juceLV2 {
+
/** Returns the name of the plugin binary file */
String getBinaryName()
{

  • return String(JucePlugin_Name).replace(" ", “_”);
  • return String(JucePlugin_Name); //.replace(" ", “_”);
    }

/** Returns plugin type, defined in AppConfig.h or JucePluginCharacteristics.h */
@@ -125,7 +127,8 @@
{
String symbol, trimmedName = name.trimStart().trimEnd().toLowerCase();

  • if (trimmedName.isEmpty())
  • // always use generic symbols because my names are dynamic

  • if (true || trimmedName.isEmpty())
    {
    symbol += “lv2_port_”;
    symbol += String(portIndex+1);
    @@ -519,33 +522,35 @@
    std::cout << "\nSaving preset " << i+1 << “/” << numPrograms+1 << “…”;
    std::cout.flush();

  •    String preset;
    
  •    // Label
       filter->setCurrentProgram(i);
    
  •    presets += "<" JucePlugin_LV2URI "#preset" + String(i+1) + "> a pset:Preset ;\n";
    
  •    presets += "    rdfs:label \"" + filter->getProgramName(i) + "\" ;\n";
    
  •    preset += "<" JucePlugin_LV2URI "#preset" + String(i+1) + "> a pset:Preset ;\n";
    
  •    preset += "    rdfs:label \"" + filter->getProgramName(i) + "\" ;\n";
    
       // State
    

#if JucePlugin_WantsLV2State

  •    presets += "    state:state [\n";
    
  •    preset += "    state:state [\n";
    
    #if JucePlugin_WantsLV2StateString
  •    presets += "        <" JUCE_LV2_STATE_STRING_URI ">\n";
    
  •    presets += "\"\"\"\n";
    
  •    presets += filter->getStateInformationString().replace("\r\n","\n");
    
  •    presets += "\"\"\"\n";
    
  •    preset += "        <" JUCE_LV2_STATE_STRING_URI ">\n";
    
  •    preset += "\"\"\"\n";
    
  •    preset += filter->getStateInformationString().replace("\r\n","\n");
    
  •    preset += "\"\"\"\n";
    
    #else
    MemoryBlock chunkMemory;
    filter->getCurrentProgramStateInformation(chunkMemory);
    const String chunkString = Base64Encode(chunkMemory);
  •    presets += "        <" JUCE_LV2_STATE_BINARY_URI "> [\n";
    
  •    presets += "            a atom:Chunk ;\n";
    
  •    presets += "            rdf:value\"\"\"" + chunkString + "\"\"\"^^xsd:base64Binary\n";
    
  •    presets += "        ]\n";
    
  •    preset += "        <" JUCE_LV2_STATE_BINARY_URI "> [\n";
    
  •    preset += "            a atom:Chunk ;\n";
    
  •    preset += "            rdf:value\"\"\"" + chunkString + "\"\"\"^^xsd:base64Binary\n";
    
  •    preset += "        ]\n";
    
    #endif
    if (filter->getNumParameters() > 0)
  •        presets += "    ] ;\n\n";
    
  •        preset += "    ] ;\n\n";
       else
    
  •        presets += "    ] .\n\n";
    
  •        preset += "    ] .\n\n";
    

#endif

     // Port values

@@ -554,19 +559,21 @@
for (int j=0; j < filter->getNumParameters(); j++)
{
if (j == 0)

  •            presets += "    lv2:port [\n";
    
  •            preset += "    lv2:port [\n";
           else
    
  •            presets += "    [\n";
    
  •            preset += "    [\n";
    
  •        presets += "        lv2:symbol \"" + nameToSymbol(filter->getParameterName(j), j) + "\" ;\n";
    
  •        presets += "        pset:value " + String(safeParamValue(filter->getParameter(j)), 8) + " ;\n";
    
  •        preset += "        lv2:symbol \"" + nameToSymbol(filter->getParameterName(j), j) + "\" ;\n";
    
  •        preset += "        pset:value " + String(safeParamValue(filter->getParameter(j)), 8) + " ;\n";
    
           if (j+1 == filter->getNumParameters())
    
  •            presets += "    ] ";
    
  •            preset += "    ] ";
           else
    
  •            presets += "    ] ,\n";
    
  •            preset += "    ] ,\n";
       }
    
  •    presets += ".\n\n";
    
  •    preset += ".\n\n";
    
  •    presets += preset;
    

    }

    return presets;
    @@ -895,7 +902,8 @@
    externalUI (nullptr),
    externalUIHost (nullptr),
    externalUIPos (100, 100),

  •        uiTouch (nullptr)
    
  •        uiTouch (nullptr),
    
  •        hostPrograms (nullptr)
    
    {
    filter->addListener(this);

@@ -1231,6 +1239,9 @@
#endif
uridMap (nullptr)
{
+#if JUCE_LINUX

  •    MessageManagerLock mmLock;
    

+#endif
filter = createPluginFilter();
jassert(filter != nullptr);

@@ -1283,6 +1294,9 @@

 ~JuceLV2Wrapper()
 {

+#if JUCE_LINUX

  •    MessageManagerLock mmLock;
    

+#endif
filter = nullptr;

     channels.free();

@@ -1719,8 +1733,12 @@
if (filter)
filter->setCurrentProgramStateInformation (data, sizeInBytes);

  •    if (ui)
    
  •    if (ui) {
    

+#if JUCE_LINUX

  •        MessageManagerLock mmLock;
    

+#endif
ui->repaint();

  •    }
    
    }

#if JucePlugin_WantsLV2StateString
@@ -1739,8 +1757,12 @@

     filter->setStateInformationString(data);
  •    if (ui)
    
  •    if (ui) {
    

+#if JUCE_LINUX

  •        MessageManagerLock mmLock;
    

+#endif
ui->repaint();

  •    }
    
    }
    #endif

@@ -2141,6 +2163,9 @@
juceLV2UI_ExtensionData
};

+} // namespace juceLV2
+using namespace juceLV2;
+
//==============================================================================
// Mac startup code…
#if JUCE_MAC
@@ -2203,10 +2228,6 @@
}
}

  • // don’t put initialiseJuce_GUI or shutdownJuce_GUI in these… it will crash!
  • attribute((constructor)) void myPluginInit() {}
  • attribute((destructor)) void myPluginFini() {}

//==============================================================================
// Windows startup code…
#else
[/code]

I applied your fixes on the distrho git code, thanks!
(not the namespace though, afaik the VST code doesn’t use it either)

Just to explain why I’m using a different namespace (in could be also in the vst wrapper , or in both) : it allows me to build an “all-in-one” plugin binary, that is at the same time a vst plugin, and an lv2 plugin

Btw I’m testing a bit with ardour 3 svn right now, it seems to work fine (except for the presets , but I think you already know that). The only issue is that it crashes when closing the plugin ui. Happens also with, for example, drumsynth.lv2. The backtrace seems to only involve ardour/suil/gtk code , though

[quote=“jpo”]Just to explain why I’m using a different namespace (in could be also in the vst wrapper , or in both) : it allows me to build an “all-in-one” plugin binary, that is at the same time a vst plugin, and an lv2 plugin

Btw I’m testing a bit with ardour 3 svn right now, it seems to work fine (except for the presets , but I think you already know that). The only issue is that it crashes when closing the plugin ui. Happens also with, for example, drumsynth.lv2. he backtrace seems to only involve ardour/gtk code , though[/quote]
Use the latest suil from SVN, it’s a known bug and has been fixed, from changelog:

[quote] * Fix crashes when wrapper widget is destroyed by toolkit before
suil cleanup function is called[/quote]

I don’t really see the point on making an “all-in-one” binary for lv2, since it has to be inside a *.lv2 folder to work. unless you symlink it from another location… ?

Just a heads-up to interested parties.
I’m doing a code cleanup and update during this week (LV2 now has more useful features).
By the end of it I should have a fully working implementation of the Juce LV2 wrapper.
This will require the very latest host versions to work, but I consider that a good thing.

Jules, can we please try to merge this wrapper next week then?
I know you don’t want the new 2 StateString calls to AudioProcessor just for LV2, so that will be optional (I will still use it internally). LV2 can work with binary chunks just as fine.
If you could start by adding the new LV2 wrapper type to the juce source code, that would be nice.
Thanks!

[quote=“falkTX”]Jules, can we please try to merge this wrapper next week then?
I know you don’t want the new 2 StateString calls to AudioProcessor just for LV2, so that will be optional (I will still use it internally). LV2 can work with binary chunks just as fine.
If you could start by adding the new LV2 wrapper type to the juce source code, that would be nice.
Thanks![/quote]

Sure, would be happy to have a look!

ok, I’ve made some big changes to the code, overall fixing and cleanup, etc etc.

Latest code is here:
https://github.com/falkTX/DISTRHO/tree/master/libs/juce-2.0/source/modules/juce_audio_plugin_client/LV2

Notes:

  • LV2 wrapper requires JucePlugin_LV2URI macro to be set. It’s used as the ID of the plugin and must be a valid URI and unique across the system.

  • Some macros have been added: (all optional)
    JucePlugin_LV2Category -> Set it as string to a valid LV2 plugin category. See http://lv2plug.in/ns/lv2core/ for valid values.
    Example: “InstrumentPlugin”. If this macro is not set and plugin is marked as synth, the wrapper will automatically add “InstrumentPlugin” class.
    JucePlugin_WantsLV2State -> If 1, the wrapper will export stateInformation to the host, otherwise it just uses parameter values.
    JucePlugin_WantsLV2Presets -> If 1, the wrapper will create a presets.ttl file with the default preset data.
    JucePlugin_WantsLV2TimePos -> If 1, the wrapper will report timePosition support.
    There’s also JucePlugin_WantsLV2StateString, but it’s an internal thing for me and Juce will probably not use it.

  • The Juce plugin type is set as ‘VST’.
    Jules, please add LV2 in AudioProcessor::WrapperType.

  • The final binary needs to be ran with a minor tool that generates its *.ttl data, see:
    https://github.com/falkTX/DISTRHO/tree/master/libs/lv2-ttl-generator

I’m still doing some testing, but please try the wrapper and let me know what you think.
It’s supposed to work on Windows, Mac and Linux, but I only tested it so far in Linux.

Very cool stuff.

If only there was an LV2 hosting class to go with it, I could probably find a use for that… :wink:

I’ve finished the missing LV2 timePos code so now the wrapper is pretty much complete afaik.
The only thing needed now is testing and bugfixing. :smiley:

I want to code a juce class for hosting LV2s as well, but it will take some time as I’m still working on LV2 for my own host.