JUCE LV2 Plugin Wrapper

Hi there.

EDIT:
We’re in the process of integrating the wrapper into official Juce code.
The latest version of the wrapper is here:

For recent discussion, skip to page 4: http://www.rawmaterialsoftware.com/viewtopic.php?f=8&t=7494&start=45#p62609


old stuff follows:

I want to jump in JUCE development and contribute with a LV2 plugin wrapper for JUCE.
(I’ll probably base this on the VST wrapper code, and borrow some from the unofficial DSSI wrapper attempt)

The most complicated thing will be to generate RDF data on-the-fly. Calf plugins do this, so I’ll borrow some code.

Let’s review the extensions needed:

  • URI Map (for events)
  • Events (for MIDI)
  • MIDI
  • UI
  • External UI (I can’t see Suil or any non-JUCE host supporting JUCE UIs natively)
  • Data Access
  • Instance Access
    And some to handle Chunk data (JUCE XML dump of plugin state)
    ^ These extensions will provide all the functionality we need.
    I’m not sure how presets work in LV2 currently.

But I have some questions regarding JUCE:

  • Can a JUCE plugin change Audio and MIDI ports or is it static?
    (this is not possible in LV2)
  • Can a JUCE plugin add new/remove parameters?
    (this will require lv2dynparam extension, which complicates things a bit and most hosts don’t support it)
  • Does JUCE support multi-plugins per binary?
    (afaik, it doesn’t. less work for me!)
2 Likes

I’ve been told that Dave Robillard is very open for supporting plugins in suil that just expose an X11 windows for their interface (instead of only GTK or Qt interfaces) so I think you should consider not taking the ‘externalui’ extension road ! Maybe you should contact him.

I decided to use both UIs - JUCE native UI and external UI.
It’s not that hard to code…

I’m still not sure if Drobilla will be ok with a JUCE UI. Last time I tried to ask him, he just left the IRC room…
Still, Suil is linux only, while LV2 is not. A (future) Windows host may want to use JUCE LV2s, and Suil won’t be there to help, so external UI makes sense.

Good News!

Auto-generating turtle files work!
Here is the output of the JUCE demo plugin, converted to ttl for LV2:

manifest.ttl:

[code]@prefix lv2: http://lv2plug.in/ns/lv2core# .
@prefix rdfs: http://www.w3.org/2000/01/rdf-schema# .

urn:Raw_Material_Software:Juce_Demo_Plugin:1.0.0
a lv2:Plugin ;
lv2:binary <Juce_Demo_Plugin.so> ;
rdfs:seeAlso <Juce_Demo_Plugin.ttl> .[/code]

Juce_Demo_Plugin.ttl:

[code]@prefix doap: http://usefulinc.com/ns/doap# .
@prefix lv2: http://lv2plug.in/ns/lv2core# .
@prefix lv2ev: http://lv2plug.in/ns/ext/event# .
@prefix lv2ui: http://lv2plug.in/ns/extensions/ui# .

urn:Raw_Material_Software:Juce_Demo_Plugin:JUCE-Native-UI
a lv2ui:JUCEUI ;
lv2ui:binary <Raw_Material_Software.so> .
urn:Raw_Material_Software:Juce_Demo_Plugin:JUCE-External-UI
a uiext:external ;
lv2ui:binary <Raw_Material_Software.so> .

urn:Raw_Material_Software:Juce_Demo_Plugin:1.0.0
a lv2:Plugin ;

lv2:port [
  a lv2:InputPort, lv2ev:EventPort;
  lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;
  lv2:index 0;
  lv2:symbol "midi_in";
  lv2:name "MIDI Input";
] ;
lv2:port [
  a lv2:OutputPort, lv2ev:EventPort;
  lv2ev:supportsEvent <http://lv2plug.in/ns/ext/midi#MidiEvent> ;
  lv2:index 1;
  lv2:symbol "midi_out";
  lv2:name "MIDI Output";
] ;

lv2:port [
  a lv2:InputPort, lv2:AudioPort;
  lv2:index 2;
  lv2:symbol "audio_in_0";
  lv2:name "Audio Input 0";
],
[
  a lv2:InputPort, lv2:AudioPort;
  lv2:index 3;
  lv2:symbol "audio_in_1";
  lv2:name "Audio Input 1";
] ;
lv2:port [
  a lv2:OutputPort, lv2:AudioPort;
  lv2:index 4;
  lv2:symbol "audio_out_0";
  lv2:name "Audio Output 0";
],
[
  a lv2:OutputPort, lv2:AudioPort;
  lv2:index 5;
  lv2:symbol "audio_out_1";
  lv2:name "Audio Output 1";
] ;

lv2:port [
  a lv2:InputPort;
  a lv2:ControlPort;
  lv2:index 6;
  lv2:symbol gain";
  lv2:name gain;
  lv2:default 1.0;
  lv2:minimum 0.0;
  lv2:maximum 1.0;
],
[
  a lv2:InputPort;
  a lv2:ControlPort;
  lv2:index 7;
  lv2:symbol delay";
  lv2:name delay;
  lv2:default 0.5;
  lv2:minimum 0.0;
  lv2:maximum 1.0;
] ;

doap:name "Juce Demo Plugin" ;
doap:creator "Raw Material Software" .[/code]

There a few things missing (presets and units), but I’ll get there later.

My current code requires some changes to JUCE plugins though, in JucePluginCharacteristics.h, I added2 more fields:

#define JucePlugin_LV2Includes "PluginProcessor.h" #define JucePlugin_LV2ClassName JuceDemoPluginAudioProcessor
This is required to build the *.ttl files, otherwise we would need to compile the plugin binary first, and somehow extract the info from it.
I would like some opinions in here though…

Here’s my ttl-generator code so far:

[code]/*

  • LV2 ttl generator for JUCE Plugins
    */

#include
#include
#include <stdint.h>

#include “JuceHeader.h”
#include “JucePluginCharacteristics.h”

#include JucePlugin_LV2Includes

// These are dummy values!
enum FakePlugCategory
{
kPlugCategUnknown,
kPlugCategEffect,
kPlugCategSynth,
kPlugCategAnalysis,
kPlugCategMastering,
kPlugCategSpacializer,
kPlugCategRoomFx,
kPlugSurroundFx,
kPlugCategRestoration,
kPlugCategOfflineProcess,
kPlugCategGenerator
};

String name_to_symbol(String Name)
{
String Symbol = Name.trimStart().trimEnd().replace(" ", “_”).toLowerCase();

for (int i=0; i < Symbol.length(); i++) {
    if (std::isalpha(Symbol[i]) || std::isdigit(Symbol[i]) || Symbol[i] == '_') {
        // nothing
    } else {
        Symbol[i] == '_';
    }
}
return Symbol;

}

String float_to_string(float value)
{
if (value < 0.0f || value > 1.0f) {
std::cerr << “WARNING - Parameter uses out-of-bounds default value -> " << value << std::endl;
}
String string(value);
if (!string.contains(”.")) {
string += “.0”;
}
return string;
}

String get_uri()
{
return String(“urn:” JucePlugin_Manufacturer “:” JucePlugin_Name “:” JucePlugin_VersionString).replace(" ", “_”);
}

String get_juce_ui_uri()
{
return String(“urn:” JucePlugin_Manufacturer “:” JucePlugin_Name “:JUCE-Native-UI”).replace(" ", “_”);
}

String get_external_ui_uri()
{
return String(“urn:” JucePlugin_Manufacturer “:” JucePlugin_Name “:JUCE-External-UI”).replace(" ", “_”);
}

String get_binary_name()
{
return String(JucePlugin_Name).replace(" ", “_”);
}

String get_plugin_type()
{
String ptype;

switch (JucePlugin_VSTCategory) {
case kPlugCategSynth:
    ptype += "lv2:InstrumentPlugin";
    break;
case kPlugCategAnalysis:
    ptype += "lv2:AnalyserPlugin";
    break;
case kPlugCategMastering:
    ptype += "lv2:DynamicsPlugin";
    break;
case kPlugCategSpacializer:
    ptype += "lv2:SpatialPlugin";
    break;
case kPlugCategRoomFx:
    ptype += "lv2:ModulatorPlugin";
    break;
case kPlugCategRestoration:
    ptype += "lv2:UtilityPlugin";
    break;
case kPlugCategGenerator:
    ptype += "lv2:GeneratorPlugin";
    break;
}

if (ptype.isNotEmpty()) {
    ptype += ", ";
}

ptype += "lv2:Plugin";
return ptype;

}

String get_manifest_ttl(String URI, String Binary)
{
String manifest;
manifest += “@prefix lv2: http://lv2plug.in/ns/lv2core# .\n”;
manifest += “@prefix rdfs: http://www.w3.org/2000/01/rdf-schema# .\n”;
manifest += “\n”;
manifest += “<” + URI + “>\n”;
manifest += " a lv2:Plugin ;\n";
manifest += " lv2:binary <" + Binary + “.so> ;\n”;
manifest += " rdfs:seeAlso <" + Binary +".ttl> .\n";
return manifest;
}

String get_plugin_ttl(String URI, String Binary)
{
// Testing, need another way to do this!!
JucePlugin_LV2ClassName* JucePlugin = new JucePlugin_LV2ClassName();

String plugin;
plugin += "@prefix doap:  <http://usefulinc.com/ns/doap#> .\n";
//plugin += "@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n";
//plugin += "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n";
plugin += "@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .\n";
plugin += "@prefix lv2ev: <http://lv2plug.in/ns/ext/event#> .\n";
plugin += "@prefix lv2ui: <http://lv2plug.in/ns/extensions/ui#> .\n";
plugin += "\n";

if (JucePlugin->hasEditor()) {
    plugin += "<" + get_juce_ui_uri() + ">\n";
    plugin += "    a lv2ui:JUCEUI ;\n";
    plugin += "    lv2ui:binary <" + Binary + ".so> .\n";
    plugin += "<" + get_external_ui_uri() + ">\n";
    plugin += "    a uiext:external ;\n";
    plugin += "    lv2ui:binary <" + Binary + ".so> .\n";
    plugin += "\n";
}

plugin += "<" + URI + ">\n";
plugin += "    a " + get_plugin_type() + " ;\n";
plugin += "\n";

uint32_t i, port_index = 0;

#if JucePlugin_WantsMidiInput
plugin += " lv2:port [\n";
plugin += " a lv2:InputPort, lv2ev:EventPort;\n";
plugin += " lv2ev:supportsEvent http://lv2plug.in/ns/ext/midi#MidiEvent ;\n";
plugin += " lv2:index " + String(port_index) + “;\n”;
plugin += " lv2:symbol “midi_in”;\n";
plugin += " lv2:name “MIDI Input”;\n";
plugin += " ] ;\n";
port_index++;
#endif

#if JucePlugin_ProducesMidiOutput
plugin += " lv2:port [\n";
plugin += " a lv2:OutputPort, lv2ev:EventPort;\n";
plugin += " lv2ev:supportsEvent http://lv2plug.in/ns/ext/midi#MidiEvent ;\n";
plugin += " lv2:index " + String(port_index) + “;\n”;
plugin += " lv2:symbol “midi_out”;\n";
plugin += " lv2:name “MIDI Output”;\n";
plugin += " ] ;\n";
port_index++;
#endif

#if JucePlugin_WantsMidiInput || JucePlugin_ProducesMidiOutput
plugin += “\n”;
#endif

for (i=0; i<JucePlugin_MaxNumInputChannels; i++) {
    if (i == 0) {
        plugin += "    lv2:port [\n";
    } else {
        plugin += "    [\n";
    }

    plugin += "      a lv2:InputPort, lv2:AudioPort;\n";
    //plugin += "      lv2:datatype lv2:float;\n";
    plugin += "      lv2:index " + String(port_index) + ";\n";
    plugin += "      lv2:symbol \"audio_in_" + String(i) + "\";\n";
    plugin += "      lv2:name \"Audio Input " + String(i) + "\";\n";

    if (i == JucePlugin_MaxNumInputChannels-1) {
        plugin += "    ] ;\n";
    } else {
        plugin += "    ],\n";
    }

    port_index++;
}

for (i=0; i<JucePlugin_MaxNumOutputChannels; i++) {
    if (i == 0) {
        plugin += "    lv2:port [\n";
    } else {
        plugin += "    [\n";
    }

    plugin += "      a lv2:OutputPort, lv2:AudioPort;\n";
    //plugin += "      lv2:datatype lv2:float;\n";
    plugin += "      lv2:index " + String(port_index) + ";\n";
    plugin += "      lv2:symbol \"audio_out_" + String(i) + "\";\n";
    plugin += "      lv2:name \"Audio Output " + String(i) + "\";\n";

    if (i == JucePlugin_MaxNumOutputChannels-1) {
        plugin += "    ] ;\n";
    } else {
        plugin += "    ],\n";
    }

    port_index++;
}

#if JucePlugin_MaxNumInputChannels > 0 || JucePlugin_MaxNumOutputChannels > 0
plugin += “\n”;
#endif

for (i=0; i < JucePlugin->getNumParameters(); i++) {
    if (i == 0) {
        plugin += "    lv2:port [\n";
    } else {
        plugin += "    [\n";
    }

    plugin += "      a lv2:InputPort;\n";
    plugin += "      a lv2:ControlPort;\n";
    //plugin += "      lv2:datatype lv2:float;\n";
    plugin += "      lv2:index " + String(port_index) + ";\n";
    plugin += "      lv2:symbol " + name_to_symbol(JucePlugin->getParameterName(i)) + "\";\n";
    plugin += "      lv2:name " + JucePlugin->getParameterName(i) + ";\n";
    plugin += "      lv2:default " + float_to_string(JucePlugin->getParameter(i)) + ";\n";
    plugin += "      lv2:minimum 0.0;\n";
    plugin += "      lv2:maximum 1.0;\n";
    // TODO - units

    if (i == JucePlugin_MaxNumOutputChannels-1) {
        plugin += "    ] ;\n";
    } else {
        plugin += "    ],\n";
    }

    port_index++;
}

if (JucePlugin->getNumParameters() > 0) {
    plugin += "\n";
}

plugin += "    doap:name \"" + String(JucePlugin_Name) + "\" ;\n";
plugin += "    doap:creator \"" + String(JucePlugin_Manufacturer) + "\" .\n";

delete JucePlugin;
return plugin;

}

int main(int argc, char *argv[])
{
String URI = get_uri();
String Binary = get_binary_name();
String BinaryTTL = Binary + “.ttl”;

std::cout << "Writing manifest.ttl...";
std::fstream manifest("manifest.ttl", std::ios::out);
manifest << get_manifest_ttl(URI, Binary) << std::endl;
manifest.close();
std::cout << " done!" << std::endl;

std::cout << "Writing " << BinaryTTL;
std::fstream plugin(BinaryTTL.toUTF8(), std::ios::out);
plugin << get_plugin_ttl(URI, Binary) << std::endl;
plugin.close();
std::cout << " done!" << std::endl;

return 0;

}
[/code]

I’ll keep working on this and keep you guys posted.

1 Like

ahah well my information was not first hand, so maybe it was a bit over optimistic :slight_smile:

Anyway, great work !

Good News!

Plugin processing (Effects) are working fine.
To do is plugin UIs (JUCE and external), MIDI and chunks.

I’ve created a git repo for this:
http://repo.or.cz/w/juce-lv2.git

Please follow the updates there.
I’ll post a screenshot here once I’ve got plugin UIs working.

1 Like

Simple rdf generation and processing already works, but things got complicated when I added some GUI functions…

Can someone clarify me what it’s the purpose of all the ‘mmLock’? (I suppose it’s to wait until processing occurs, then do the GUI stuff?)

And is MessageSharedThread really needed on Linux?
I’m not sure what it does, but I assume it handles multiple instances of the same plugin?

Any help is appreciated, thanks!

take what I say with a grain of salt, maybe Jules will correct me, but as far as I know:

mmLock is a lock to prevent race conditions between the juce message thread, and other threads (the host will call your lv2 callbacks from a thread which will never be the juce message thread so you have to take care of any potential race condition)

The shared message thread stuff is quite specific to linux / x11 , it is used by juce for its event loop. It is shared by all instances of your plugin loaded in the host application (save some ressources, and allow them to communicate).

ok, until he posts here, help me just a bit here

So I should just basically add it before any GUI call (like changing parameters) ?
GUI → Host should be safe I guess, right?

But why only Linux needs/have this…? Is it really required?

After a small testing, I realized that I should probably do initializejuce_GUI as soon as the DLL loads (as done with the VST wrapper).
This is a little bad for gui-less plugins…

well I don’t recall the details but I think nothing will work if you don’t have the sharedmessagethread stuff running. On macos, juce uses the host event loop, on windows it uses whatever thread is used when instanciating the plugin but on linux there is no convention for that, so juce has to create its own thread for sending / receiving its internal messages and X11 messages.

initialisejuce_gui should work fine when no X11 display is available

I think your reference should be the vst wrapper, which works pretty well on linux. The dssi wrapper was basically a stripped down version with gui and win32/macos removed.

Thanks for the clarification, that makes sense.

Cool, then I’ll initialize it as soon as the plugin loads

The dssi wrapper is useful cause DSSI has some similarities to LV2 (but not in the GUI stuff though).
I’ll try to make this as close to the VST wrapper as possible, just to be safe.

ok, there is progress.

Plugin processing already works fine (code heavily based on the VST wrapper), including MIDI input and output.

The UI is a bit more complicated, but at least it’s now being shown/hidden on demand (via External UI).
JUCE UI is ok, since we just need to pass the our JUCE Component pointer, but for external UI we need to create our own window.
(Sadly, there’s not a single JUCE app that loads LV2… :frowning: )

Here’s a mandatory screenshot - Ardour using JUCE Demo and TAL-Reverb-II plugins, with their UI shown via external UI.

it’s working!!!

here’s a testing 64bit plugin you can try:
http://kxstudio.sourceforge.net/tmp/EQinox.lv2.7z

Known issues:

  • external UI does not close
  • no chunk save support (only port values for now)
1 Like

cool! Nice work!

I’m very close to finish this baby, just need an extension for chunk stuff, and some listener for the close button.

Jules, would you consider adding this to the source tree when ready?

1 Like

Sure, thanks very much!

ok, things are advancing where I feel soon I’ll be ready to show the lv2-wrapper to everyone.
with the juce-2.0 modules now is a great time to introduce new features I guess…

Jules, to make LV2 plugins work juce needs some small mods (check for Build_LV2 macro and 2 new Processor functions), here’s a patch:

[code]diff --git a/modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h b/modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h
index a942404…fcb9e02 100644
— a/modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h
+++ b/modules/juce_audio_plugin_client/utility/juce_CheckSettingMacros.h
@@ -26,7 +26,7 @@
// The following checks should cause a compile error if you’ve forgotten to
// define all your plugin settings properly…

-#if ! (JucePlugin_Build_VST || JucePlugin_Build_AU || JucePlugin_Build_RTAS || JucePlugin_Build_Standalone)
+#if ! (JucePlugin_Build_VST || JucePlugin_Build_AU || JucePlugin_Build_RTAS || JucePlugin_Build_Standalone || JucePlugin_Build_LV2)
#error “You need to enable at least one plugin format!”
#endif

diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp
index 03e0ab4…4d797f5 100644
— a/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp
+++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.cpp
@@ -246,6 +246,16 @@ void AudioProcessor::setCurrentProgramStateInformation (const void* data, int si
}

//==============================================================================
+String AudioProcessor::getStateInformationString ()
+{

  • return String::empty;
    +}

+void AudioProcessor::setStateInformationString (const String& data)
+{
+}
+
+//==============================================================================
// magic number to identify memory blocks that we’ve stored as XML
const uint32 magicXmlNumber = 0x21324356;

diff --git a/modules/juce_audio_processors/processors/juce_AudioProcessor.h b/modules/juce_audio_processors/processors/juce_AudioProcessor.h
index f5c224c…37866c2 100644
— a/modules/juce_audio_processors/processors/juce_AudioProcessor.h
+++ b/modules/juce_audio_processors/processors/juce_AudioProcessor.h
@@ -524,6 +524,12 @@ public:
/
virtual void setCurrentProgramStateInformation (const void
data, int sizeInBytes);

  • //==============================================================================

  • /** LV2 specific calls, saving/restore as string. */

  • virtual String getStateInformationString ();

  • virtual void setStateInformationString (const String& data);

    //==============================================================================
    /** Adds a listener that will be called when an aspect of this processor changes. */
    [/code]

plugins will have to add support for get/setStateInformationString if they want proper lv2 state save (otherwise only normal parameter values are saved in the host).

Ok… but I don’t understand the need for the string methods. Can’t you just convert the binary data to a base-64 string?

Yes, it’s possible, but since LV2 uses strings by default it feels kinda awkward to go back to binary data (and thus we don’t have to worry about alignment, endian size or any other “problem” when using blobs)

Note that lv2 presets are on the host-side, by using *.ttl files. This means the data can be read by users, so for this it also makes sense to have strings.

But if you just convert the binary data to/from strings, then all existing plugins will work without modification, which is surely a good thing?